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.
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 | The Stripe secret key. Never stored in a custom field, never readable back, not even by the admin who entered it. |
| 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 eleven 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 9 tells you the account is still missing. Start at step 1; you do not need a payment account to begin.
Steps 1–6 read Not checked yet until you finish step 9, 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 six 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 eleven 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. 2. Work down the guide. Steps 1–6 build the credential pair in Setup → Named Credentials: an External Credential with a Named Principal carrying your secret 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 9 below.The panel suggests a name for both credentials (
Stripe_Auth_PublicandStripe_Auth_Admin, andStripe_Public/Stripe_Adminfor the two Named Credentials) — two credentials cannot share a name. Once you have built them, it stops suggesting and prints the names your org actually uses.Step 2 asks for a different key on each credential and says which is which: a charge-only restricted
rk_…key on the public credential, and a back-office key on the admin one — best a second restricted key scoped to refunds and reads, or your fullsk_…secret key if you would rather not scope one. This is the one place where getting them the wrong way round is silent — both keys authenticate perfectly well, so an admin key on the public credential works, and hands the public internet a refund button. Step 10 is what catches it. 3. Step 5 is Allowed Namespaces for Callouts, and it is the one step 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. Open the Named Credential → Edit → Allowed Namespaces for Callouts and add the namespace the guide prints for you; after saving it appears under Managed Package Access on the credential. 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 7 creates a permission set of your own carrying the External Credential Principal Access grant, and assigns it to the staff who take payments or issue refunds. In split mode there are two grants to make — one per credential — and the step grades both.The same set also needs 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 8 assigns the public credential's permission set 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 9 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, Test and Delete on the row. That is deliberate: step 9 is about the one account these eleven 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 eleven steps to another.
The form asks for two credentials, and both are required:
Field Used by Grant it to Public named credential Taking payments — the charge path a public donation page reaches Staff and your site guest user (step 8) Admin named 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.
Splitting them is what keeps an anonymous visitor's principal from reaching a key that can issue refunds. If you'd rather run a single key for now, point both fields at the same credential — the app accepts it; it just gives you no separation, and the guide grades the affected steps amber rather than green to say so. 7. Step 10 proves your public key really is scoped charge-only. It is on demand: press the button and the app makes a single read-only call that a charge-only key is not allowed to make. If that call succeeds, the credential your guest user holds carries a key with refund rights — your admin key or your secret key — and the step reports a failure rather than a pending item. 8. Step 11, 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.
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_… |
The public External Credential, and — recommended — the admin one too | Only what you explicitly grant it, resource by resource. Each credential gets its own key with its own scopes. |
| Secret | sk_live_… / sk_test_… |
The admin External Credential, if you would rather not scope a second restricted key | Everything your Stripe account can do — unrestricted, with no scopes to choose. |
Both credentials can be restricted keys, and that is the recommended setup: two keys, each scoped to its own job. The full secret key remains supported on the admin credential — it needs no scoping and nothing a donor touches ever reaches it — but it can do anything your Stripe account can do, so prefer the scoped key where you are willing to build one.
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. Whichever kind of key you put on the admin credential, only staff-triggered and scheduled work touches it: issuing refunds, reading disputes, the nightly reconcilers, off-session recurring renewals, and Test connection. Nothing a donor's browser does reaches it. Stripe itself recommends using restricted keys wherever a full secret key isn't required, which is why the scoped admin key below is the recommendation and the unrestricted key is the fallback.
Build the whole thing in test mode first
A key beginning rk_test_ or sk_test_ only ever reaches Stripe's test mode: no real card is
charged and no real money moves (Stripe: test mode). Test
and live are separate worlds — a PaymentIntent created with a test key is invisible to a live
key, and vice versa. Connect a test-mode 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 public key#
Scope it to exactly this and nothing more — leave every other resource Stripe offers on None:
| Stripe resource | Public (restricted) key | Why |
|---|---|---|
| Customers | Write | Creating and updating the donor's Stripe customer |
| PaymentIntents | Write | Starting and confirming a donation or ticket purchase |
| SetupIntents | Write | Saving a card for a recurring gift |
| PaymentMethods | Write | Attaching and reading the saved card |
| Charges | Read | Confirming a gift server-side, and noticing a refund issued in the dashboard |
| Balance transactions | Read | Recording the processor fee and the net amount on each gift |
| Refunds | None | The whole point — see below |
| Disputes | None | Read on the back-office key only |
The two Read rows are not optional. Confirming a payment re-reads the PaymentIntent with
latest_charge.balance_transaction expanded, so a key scoped without them either fails the read
outright or — worse, because it is silent — returns it without the expansion, leaving every
donation with a blank processor fee and net, and leaving the refund reconciler blind to refunds
issued in the Stripe Dashboard.
The public 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#
You can leave the admin credential on your full secret key and skip this section entirely. If you would rather scope it — and you should, for the same reason you scoped the public one — build a second restricted key with exactly these permissions:
| Stripe resource | Admin (restricted) key | Why |
|---|---|---|
| Refunds | Write | Issuing a refund from the donation record, and the refund reconciler's reads |
| PaymentIntents | Write | Off-session renewals of recurring gifts, and the orphan sweep's re-reads |
| Disputes | Read | Recording a chargeback against the donation it belongs to |
| Charges | Read | Matching a refund or dispute back to the gift that produced it |
| Balance transactions | Read | The fee and net figures on a refund |
| Customers | Read | Resolving the donor behind a saved card at renewal time |
| Payment methods | Read | Reading the saved card a renewal charges |
Leave everything else on None.
One thing a scoped admin key cannot do — and it is fine
Test connection first asks Stripe for your account profile, so it can print your account name and mode back to you. That read sits behind its own Stripe permission that a correctly scoped restricted key is not granted — so when Stripe refuses it, the app falls back to a second read-only call the admin key must be able to make anyway (listing refunds) and reports the key as working, without the account name. That is the expected result on a scoped key. Never widen a key just to make this button print a name.
Step 10 of the guide checks the Refunds: None row for you. Stripe answers a restricted key with
403 Forbidden when it reaches for something it has no permission for, and that refusal is the
only positive proof available that the key an anonymous visitor's session can reach cannot issue
a refund.
One credential, or two?#
Two is the default and the safer answer. One is supported.
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 public field names, so a single shared credential means an anonymous visitor's session holds a key that can issue refunds. The Payments panel marks that configuration amber rather than green and says so plainly, every time you open it. It will not stop you, and it will not pretend the setup is finished either.
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 amber already said.
Split them when you can. 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 6 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. Two live calls settle the rest between them: step 10's probe proves the key your guest user holds really is restricted, and step 11's Test connection proves the whole chain — key, header, callout options, namespace grant — actually works end to end. Both are read-only and move no money.
Public donations need the guest user grant
Granting the public credential to your site's guest user (step 8) 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 8 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 public 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
Stripe_Public_Access, so public donation pages can charge a card. Confirm that set grants the PUBLIC credential only — a guest holding the admin credential can issue refunds. "Prove the public key is restricted" proves it from the other side.
That state is exactly what running two credentials exists to prevent. Whether the admin credential carries a scoped key or the full secret key, anything reaching it can refund a charge and read your back-office ledger — and on a full secret key, 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 10 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 public credential — either your admin key or your secret key.
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 8. 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 test mode 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 that a campaign still points at can't be deleted; the Payments panel says so rather than letting the reference break.
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. See
Stripe → for the architecture in full.
Next#
-
Finish setup
Org detection, record types, and health checks.
-
Understand payment records
How Opportunities, staging records, and payment status fields relate.
-
Stripe architecture reference
The gateway interface, reconciliation jobs, and refund/dispute handling.