Skip to main content

Payments in Salesforce

How to read what a payment leaves on your records and how the package decides who paid, how much, and what it was for.

Overview​

Flourish Payments adds no custom objects. Each payment lands on an 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.

A payment can arrive in several ways:

  • Through a payment form on your website
  • Through a payment link
  • As an invoice
  • As a subscription's payment
  • As a charge to a card saved earlier

Products, invoices, and subscriptions covers invoices, subscriptions, and payment links. This page covers what every payment has in common.


Reading a payment on the record page​

Two read-only components go on the Opportunity record page. See Setup.

ComponentWhat it shows
Flourish Payments: Stripe Sync ViewerAny subscription this Opportunity started sits on top. Below it is one card per payment. Each card shows the source, amount, status, and Stripe IDs. An invoice's card adds its number and a link to pay it. A refunded payment adds the refund's ID, amount, and date.
Flourish Payments: Custom Data ViewerEverything the form collected that isn't a standard payment field. It also shows how the charge amount was decided. Amounts show as currency.

Each payment card names its source: Checkout, Invoice, Subscription payment, or Payment link.

The Stripe Sync card for a $300.00 checkout marked Completed. It shows the session, payment intent, and customer IDs and when the checkout was created and completed. Below them is a Refund marked Succeeded for $150.00 with its refund ID and date.
StatusMeaning
PendingStarted with no result yet. An unpaid invoice is Pending, too. It shows a note if a payment attempt failed.
ProcessingA bank payment that's still clearing.
CompletedPaid. The amount shown is what Stripe charged.
FailedThe payment didn't go through.
ExpiredThe customer never finished the checkout and Stripe closed it.
RefundedRefunded in full. A partial refund leaves the payment Completed and shows the refunded amount on the card.
VoidAn invoice you voided in Stripe.
UncollectibleAn invoice you marked uncollectible in Stripe.

Both components are read-only on purpose. Nothing on an Opportunity should be edited to change a payment. Stripe is the record of what happened, and these fields copy it.


The fields​

Your org shows these API names with the package's toflourish__ prefix.

On the Opportunity:

FieldAPI nameWhat it is
Stripe Synctoflourish__pay_Stripe_Sync__cThe payment history and any subscription for this Opportunity in JSON. Written by the package. Never edit it by hand.
Payment Custom Datatoflourish__pay_Custom_Data__cJSON of everything the form collected that the package doesn't handle itself and of the pricing breakdown.
External Idstoflourish__pay_External_Ids__cAn index the package uses to match an incoming Stripe event back to this record. Internal bookkeeping.
Payment Modeltoflourish__pay_Payment_Model__cHow the form should collect payment for this Opportunity. See below.
Initial Stripe Checkout Datetimetoflourish__pay_Initial_Checkout_Completed_DT__cStamped when the first checkout gets past pending. It never changes after that. This is the field that tells you a record came through a payment form rather than being entered by hand.
Subscription Opportunitytoflourish__pay_Subscription_Opportunity__cOn a subscription's renewal payment, the Opportunity the subscription started from. The starting Opportunity lists them under Subscription Payments.

On the Contact:

FieldAPI nameWhat it is
Stripe Customer IDtoflourish__pay_Stripe_Customer_Id__cThe Stripe customer this person is. Filled in from their payments.

On the Product:

FieldAPI nameWhat it is
Stripe Synctoflourish__pay_Stripe_Sync__cThe product's Stripe ID, its prices, and its payment links in JSON. Written by the Flourish Payments: Stripe Product card.

Who the payment gets attached to​

The Contact.

  • Matching: by email address. If several match, the oldest is used, and if none do, one is created.
  • Blank fields on that Contact are filled in from the form. Fields that already have values are left alone.
  • Stripe Customer ID: filled in from the Stripe customer a payment uses, and a later payment can replace it with a newer one. Stripe customers aren't merged, so a person who pays several times can have several in Stripe. All of them belong to the same Contact.

The company. If the form collected a company name, an Account is matched by exact name (oldest first) or created. The payer is then linked to it:

  • On NPSP: with a primary affiliation. The Contact stays in their household.
  • In other orgs: the Account becomes the Contact's Account if the Contact doesn't have one already.

The Opportunity gets the Account either way.

The Opportunity.

  • Name: built from the item, the payer, and the date.
  • Stage: comes from Default Opportunity Stage if you set one. Otherwise it's the first of Pledged, Prospecting, Qualification, and New that's an open stage in your org, or another open stage if none is.
  • Record type: comes from Opportunity Record Type if you set one.
  • Time zone: Opportunities created from payment forms, payment links, and subscription renewals are dated in the time zone of your connector site's guest user. Set that user's time zone to your org's so a payment made late in the evening isn't dated the next day.

Payment models​

By default, a payment form asks the customer for an amount. That suits donations and memberships. Set Payment Model on an Opportunity instead, and a form opened for that Opportunity collects against what's already on the record.

Payment ModelWhat the customer sees
Opportunity OnlyThe Opportunity's amount, which they can't change.
Summed PaymentsThe related payment records as read-only line items and their total.
Selective PaymentsThe same line items as checkboxes. All start ticked, and the total follows what's ticked.
Dynamic PaymentsAn amount box per line item. Each takes up to what's still owed on it.

Leave the field blank for a form that asks for an amount.


Paying an existing Opportunity​

Send a customer to 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 a payment record's ID to pay just that payment:

https://example.com/pay?flpayoid=006xx000000abcd&flpaypid=a01xx000000efgh
ParameterWhat it does
flpayoidThe Opportunity to pay. The form loads its payment model, its amount, and its payment records.
flpaypidOptional. A payment record on that Opportunity. The form shows only that payment and charges what's still owed on it. The record is marked paid once the payment completes. If the Opportunity doesn't owe it, the page says there's nothing to pay and doesn't charge the Opportunity instead. That covers a record that's paid, written off, or canceled and one on another Opportunity.

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. A system outside Salesforce can then send someone to pay using the reference it already holds.

The page has to be built for this. See Payment widget.


What ends up in Payment Custom Data​

The package handles these payload keys itself, and they don't 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, priceId, interval, intervalCount

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 (how the amount was decided: flow, records, client, or price).

The Payment Custom Data card for a donation. A Filter fields box is at the top. Custom Fields lists Fund, Dedication, Cover Fee (Yes), and a Pricing Source of client. Pricing lists a base amount of $100.00, a fee of $3.00, and a total of $103.00.

Server-side pricing​

The form shows the customer a price, but Salesforce decides what's charged before Stripe is called. It decides in this order:

  1. One payment record. A checkout that sends a paymentId with its opportunityId charges what's still owed on that payment record. A pay link's flpaypid sends it. The record is marked paid once the payment completes.
  2. Pricing Flow. If Pricing Flow Name is set, that Flow decides the amount outright.
  3. Payment model. If the Opportunity's Payment Model is Summed Payments, Selective Payments, or Dynamic Payments, the amount comes from its payment records whatever the page sends. Summed Payments charges every payment the Opportunity still owes. Each entered amount is checked against what's still owed on its payment. A form opened for an Opportunity Only Opportunity charges the Opportunity's amount.
  4. The submitted amount. For a donation or membership form, the amount the customer chose.

Only payment records the Opportunity still owes can be charged. One that's paid, written off, canceled, or on another Opportunity stops the checkout, and the customer sees "One of these payments isn't open anymore. Please refresh the page and try again."

A fee the customer opted into is added on top of whichever of these applies and is never counted twice.

A form that sells a Stripe price works differently. The price is the amount, and Stripe charges what it says.

Writing a pricing Flow​

Build an Autolaunched Flow and put its API name in Pricing Flow Name.

DirectionVariableTypeNotes
InputpayloadTextJSON of everything the form submitted.
OutputamountNumberRequired. The amount to charge in dollars. Any fee the customer opted into is added on top.
OutputpricingBreakdownTextOptional. JSON kept on the Opportunity in Payment Custom Data.

Stripe charges whatever the Flow returns plus any fee the customer opted into. Use it for rates looked up from another object, tiered discounts, proration, or an org-specific minimum.


Orgs with payment records​

The package works out what your org has and adapts. There's nothing to configure.

OrgWhat it uses
NPSPThe NPSP Payment object. A payment model offers the payments that aren't paid or written off. A completed checkout marks each payment record it paid as paid, and a full refund marks those written off. A payer's company is linked by affiliation.
Nonprofit CloudThe standard Payment object. A payment model offers the payments that aren't Paid or Canceled. A completed checkout sets each payment record it paid to Paid, and a full refund sets those to Canceled.
Anything elseThe Opportunity alone. There are no payment records.

A Summed Payments or Selective Payments checkout marks each payment record it charged as paid. A Dynamic Payments checkout marks a payment paid in full as paid and splits one paid in part. A new payment record holds the amount paid and is marked paid, and the original keeps the rest, still owed.


What the webhook changes​

Stripe reports each outcome to your org, and that report is what moves the record.

Stripe eventWhat happens
checkout.session.completed, paidPayment marked Completed with the amount Stripe charged and Opportunity set to Closed Won. The payment records the checkout paid are marked paid. For a subscription, the subscription is recorded, too. For a payment link, the Opportunity is created first.
checkout.session.completed, not yet paidFor methods that settle later, such as bank debits, the payment is marked Processing. Nothing else moves yet.
checkout.session.async_payment_succeededA payment that has now cleared is marked Completed with the amount Stripe charged. The Opportunity is set to Closed Won, and the payment records the checkout paid are marked paid.
checkout.session.async_payment_failedPayment marked Failed. The Opportunity is set to Closed Lost unless it's already won or holds another payment that's Pending, Processing, or Completed.
checkout.session.expiredPayment marked Expired. The Opportunity is set to Closed Lost on the same terms as a failed payment.
charge.refundedThe refund's ID, amount, and date are recorded. A full refund marks the payment Refunded. Each payment record the checkout marked paid is then no longer paid or owed. NPSP's are marked written off, and Nonprofit Cloud's are set to Canceled. The Opportunity is set to Closed Lost only if it holds no other payment that's Pending, Processing, or Completed. Otherwise its stage stays where it is, so refunding one installment leaves the others owed. On NPSP, an Opportunity going Closed Lost also makes NPSP write off its unpaid payments. A partial refund, or one still pending, leaves the payment Completed, its payment record paid, and the stage where it is.
invoice.paidAn invoice's payment is marked Completed and its Opportunity set to Closed Won. For a subscription, the payment is recorded (see Subscriptions).
invoice.payment_failedThe failed attempt is noted on the invoice's payment or on the subscription.
invoice.voided, invoice.marked_uncollectibleThe invoice's payment marked Void or Uncollectible. The stage doesn't change.
customer.subscription.created, .updated, .deletedThe subscription on its Opportunity is kept current: status, price, next payment date, cancellation, and Canceled when it ends.

If the customer opted to save their card, the webhook also captures it. The reusable token is stored on the Opportunity, and the Contact's Stripe Customer ID is filled in. To charge it later, see Flow actions.

A payment stuck at Pending long after the customer paid means the webhook isn't 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.