Payments in Salesforce
What a checkout leaves behind on your records, and how to read it.
Flourish Payments adds no custom objects. One checkout produces one Opportunity, and everything Stripe reports about it is written back onto that Opportunity. Where your org already has payment records, the package uses those rather than inventing its own.
Reading a payment on the record page
Two read-only components go on the Opportunity Lightning record page. Add them in the Lightning App Builder.
| Component | What it shows |
|---|---|
| Flourish Payments: Stripe Sync Viewer | One card per checkout: amount, status, the Stripe IDs, and the timestamps. A refunded payment also shows its refund amount and date. |
| Flourish Payments: Custom Data Viewer | Everything the form collected that is not a standard payment field, plus how the charge amount was decided. |
Statuses in the sync viewer:
| Badge | Meaning |
|---|---|
| Pending | The checkout was created and no result has come back yet. A payment still clearing shows here too. |
| Completed | Paid. |
| Failed | The payment did not go through. |
| Expired | The customer never finished the checkout and Stripe closed the session. |
| Refunded | Refunded in full. A partial refund leaves the payment Completed and shows the refunded amount on the card. |
Both components are read-only on purpose. Nothing on an Opportunity should be edited to change a payment; Stripe is what happened, and these fields are the record of it.
The fields
On the Opportunity:
| Field | API name | What it is |
|---|---|---|
| Stripe Sync | pay_Stripe_Sync__c | The payment history for this Opportunity, as JSON. Written by the package. Never edit it by hand. |
| Payment Custom Data | pay_Custom_Data__c | Everything the form collected that the package does not handle itself, as JSON, plus the pricing breakdown. |
| External Ids | pay_External_Ids__c | An index the package uses to match an inbound Stripe event back to this record. Internal bookkeeping. |
| Payment Model | pay_Payment_Model__c | How the form should collect payment for this Opportunity. See below. |
| Initial Stripe Checkout Datetime | pay_Initial_Checkout_Completed_DT__c | Stamped once, on the first checkout that gets past pending. This is the field that tells you a record came through a payment form rather than being entered by hand. |
On the Contact:
| Field | API name | What it is |
|---|---|---|
| Stripe Customer ID | pay_Stripe_Customer_Id__c | The Stripe customer this person is. Contacts are matched by email and the ID is filled in when the checkout completes. |
Who the payment gets attached to
The package matches a Contact by email address and uses the oldest match. Blank fields on that Contact are filled in from the form; fields that already have values are left alone. If no Contact matches, one is created.
If the form collected a company name, an Account is matched by name or created, and on NPSP the package also creates the affiliation between the two.
The Opportunity is named from the item, the payer and the date. Its stage comes from Default Opportunity Stage if you set one, and otherwise from the first open stage in your org. Its record type comes from Opportunity Record Type if you set one.
Payment models
By default a payment form asks the customer for an amount, which suits donations and memberships. Set Payment Model on an Opportunity instead and the form collects against what is already on the record.
| Payment Model | What the customer sees |
|---|---|
| Opportunity Only | The Opportunity's amount, read only. |
| Summed Payments | The related payment records as read-only line items, with the total. |
| Selective Payments | The same line items as checkboxes, all ticked to start. The total follows what is ticked. |
| Dynamic Payments | An editable amount per line item, capped at what is still owed on each. |
Leave the field blank for a form that asks for an amount.
Paying an existing Opportunity
Point a customer at your payment page with the Opportunity ID in the URL and the form opens against that record instead of creating a new one:
https://example.com/pay?flpayoid=006xx000000abcd
Add flpaypid to collect against one specific payment record:
https://example.com/pay?flpayoid=006xx000000abcd&flpaypid=a0Bxx000000efgh
| Parameter | What it does |
|---|---|
flpayoid | The Opportunity to pay. The form loads its payment model, its amount, and its payment records. |
flpaypid | Optional. A single payment record to pay. The checkout is then tracked against that record rather than the Opportunity as a whole. |
flpayoid accepts a proxy ID as well as a Salesforce ID. If the Opportunity carries a toflourish__Lim_Order_Id__c value, that value works in the URL too, so a system outside Salesforce can send someone to pay using the reference it already holds.
What ends up in Payment Custom Data
The package handles these payload keys itself, and they do not appear in Payment Custom Data:
opportunityId, paymentId, paymentModel, customerId, email, firstName, lastName, companyName, amount, baseAmount, feeAmount, paymentCurrency, itemName, itemDescription, quantity, mode, returnUrl, clientReferenceId, collectPhoneNumber, metadata, selectedPaymentIds, paymentAmounts, setupFutureUsage
Every other field your form collects is written to Payment Custom Data as it arrives. A form that asks for a fund, a dedication or an employer match needs no Apex and no configuration: add the field to the form and the value is on the Opportunity. Read it out in Flow with Get Custom Data Value.
Alongside the form fields, the package writes pricing, the breakdown behind the amount, and pricingSource, which says how the amount was decided: flow, records or client.
Server-side pricing
The form shows the customer a price, but the charge amount is always recomputed in Salesforce before Stripe is called, so a tampered-with page cannot change what is charged. The amount is decided in this order:
- Pricing Flow. If Pricing Flow Name is set, that Flow decides the amount outright.
- Payment model. If the Opportunity has a Payment Model, the amount comes from the record: the Opportunity's amount, or the sum of the selected payment records, with each entered amount checked against the balance still owed.
- The submitted amount. For a donation or membership form, the amount the customer chose.
A fee the customer opted into is added on top of whichever of these applies.
Writing a pricing Flow
Build an Autolaunched Flow and put its API name in Pricing Flow Name.
| Direction | Variable | Type | Notes |
|---|---|---|---|
| Input | payload | Text | JSON of everything the form submitted. |
| Output | amount | Number | Required. The total to charge, in dollars. |
| Output | pricingBreakdown | Text | Optional. JSON kept on the Opportunity in Payment Custom Data. |
The Flow has full authority: whatever it returns is what Stripe charges. Use it for rates looked up from another object, tiered discounts, proration, or an org-specific minimum.
The help text on the Pricing Flow Name field describes an
amountInCentsoutput in whole cents. That is out of date. The Flow is read for a variable namedamount, in dollars.
Orgs with payment records
The package works out what your org has and adapts, with nothing to configure.
| Org | What it uses |
|---|---|
| NPSP | The NPSP Payment object. Unpaid payments are what a payment model offers, and a completed checkout marks them paid. Account and Contact get an affiliation. |
| Nonprofit Cloud | The standard Payment object. Payments not already paid are offered, and a completed checkout sets their status to Paid. |
| Anything else | The Opportunity alone, with no payment records. |
What the webhook changes
Stripe reports the outcome of a checkout to your org, and that report is what moves the record.
| Stripe event | What happens |
|---|---|
checkout.session.completed, paid | Payment marked Completed, Opportunity set to Closed Won, payment record marked paid. |
checkout.session.completed, not yet paid | Payment marked as clearing, for methods that settle later such as bank debits. Nothing else moves yet. |
checkout.session.async_payment_succeeded | As above for a payment that has now cleared. |
checkout.session.async_payment_failed | Payment marked Failed, Opportunity set to Closed Lost. |
checkout.session.expired | Payment marked Expired. The Opportunity is left where it is. |
charge.refunded | The refund amount and date are recorded. A full refund marks the payment Refunded, sets the Opportunity to Closed Lost, and unmarks the payment record as paid. A partial refund records the amount and changes nothing else. |
If the customer opted to save their card, the webhook is also where that card is captured: the reusable token is stored on the Opportunity and the Contact's Stripe Customer ID is filled in. Charging it later is Flow actions.
A payment stuck at Pending long after the customer paid means the webhook is not arriving. Check the endpoint's delivery attempts in the Stripe dashboard against Setup.
If a record write fails while a customer is checking out, the checkout still goes through: the customer is never blocked by a Salesforce error, and the webhook reconciles the record afterwards.