Home/Resources/Integration
Integration11 min read

Salesforce + Stripe: payment architecture done right.

Checkout, invoicing, dunning and refunds each touch Salesforce differently — and webhook idempotency, PCI scope and payout reconciliation decide whether the integration holds up when money is actually moving.

AJ

Aleš Jeník — Salesforce Enterprise Architect · 12+ years building revenue systems on Salesforce · Clouderia

Key takeaways
  • Stripe is the system of record for money; Salesforce for the customer and the expectation. Update CRM state only from confirmed webhook events — never from redirect URLs or synchronous API responses.
  • Ingest webhooks through a verify–acknowledge–queue pipeline and upsert on Stripe IDs, so at-least-once delivery is safe by construction.
  • Event order is not guaranteed: confirm state transitions against the Stripe API instead of trusting arrival order.
  • Only tokens and display metadata belong in Salesforce — a single card number in a case field drags your CRM into PCI scope.
  • Stamp Salesforce invoice and order IDs into Stripe metadata at creation time; reconciliation lives or dies on it.
  • Give every subscription exactly one owner — Stripe Billing for self-serve, Salesforce CPQ or Revenue Cloud for negotiated contracts — and mirror read-only in the other direction.

One boundary, two systems of record

Stripe is the system of record for money movement: what was charged, what settled, what was refunded, what the fees were. Salesforce is the system of record for the customer and the commercial expectation: who they are, what they bought, what they owe, who is responsible when something goes wrong. Nearly every Stripe–Salesforce failure we are called into starts with that boundary being blurred — opportunities marked paid before money confirmed, card data creeping into CRM fields, two systems each convinced they own the subscription. This article maps the four places payments legitimately touch Salesforce, then works through the plumbing that keeps the boundary honest: webhook ingestion, idempotency, PCI scope and reconciliation. It draws on the same discipline we apply across our Salesforce integration practice.

Where payments touch Salesforce

Checkout

The card form is never Salesforce's job. Whether the entry point is a quote, an order, an Experience Cloud portal or a Commerce Cloud storefront, Salesforce's role is to create the payment intent or checkout session server-side — through a Named Credential, never with keys in code — hand the customer to a Stripe-hosted surface, and record the intent identifier against the order. Payment confirmation must come back exclusively through webhooks. The redirect return URL is a courtesy, not a signal: customers close tabs, networks drop, and a success page that writes CRM state will eventually write fiction.

Invoicing

Decide who renders the invoice, and let the other system mirror. If invoices are generated in Salesforce or your ERP, Stripe is payment rails: finalized amounts are pushed across and collected against saved payment methods, and the document of record stays on your side. If Stripe Invoicing generates them — common for self-serve products — Salesforce holds a read-only mirror of invoice status so sales and support see reality without owning it. The one configuration that always ends badly is both systems producing invoices for the same charge: finance ends up arbitrating between two numbering schemes and two ideas of what is outstanding.

Dunning

Split machine dunning from human dunning. Stripe's retry machinery is good at the mechanical part: re-attempting failed cards on an optimized schedule and prompting customers to update payment methods. Salesforce owns the relationship part: the overdue B2B invoice that needs a call, the collection workflow with tasks, escalation and an audit trail. The seam between them is the failed-payment webhook — it creates the CRM-side signal that a human process starts from, while the truth about whether cash arrived stays in Stripe. Our CollectiPro product exists precisely because the human half of collections deserves its own tooling inside Salesforce.

Refunds

Refunds should begin life as Salesforce records — a refund request tied to a case, with approval, reason and an audit trail — and be executed through the Stripe API. The subtle part is state: a refund can fail asynchronously after the API call succeeds, so Salesforce must not flip invoice or order status on the synchronous response. Model the request as its own object with requested, submitted, confirmed and failed states, and let the refund webhook drive the final transition. This is the same discipline as checkout, applied in reverse: the API call expresses intent, the webhook confirms fact.

Webhook ingestion: at-least-once, out of order, forever

Stripe delivers events at-least-once, retries failures with backoff over multiple days, and does not guarantee ordering. Any ingestion design that assumes exactly-once, in-order delivery is wrong on day one and merely lucky until month three. There are three realistic architectures for getting events into Salesforce.

The first, and our default for anything business-critical, is a thin ingestion service or middleware in front of Salesforce. It verifies the event signature at the edge, acknowledges within Stripe's timeout before doing any heavy work, writes the event to a durable queue, and delivers into Salesforce at a controlled rate via upsert. This decouples Stripe's retry schedule from Salesforce availability, absorbs bursts without burning API allocation, and gives you one place to implement dedupe and replay.

The second is direct delivery to an Apex REST endpoint exposed through a public site. It is workable at low volume, but be honest about the trade-offs: signature verification runs in a guest-user context you must deliberately harden, synchronous processing lives inside governor limits, and every Stripe retry burst lands on your org unfiltered. The third is an AppExchange connector — reasonable when its object model matches yours, but audit its webhook coverage and its reconciliation story before you commit; a connector that drops event types silently is worse than none.

Whichever path carries the events, idempotency is non-negotiable in both directions. Inbound, persist the Stripe event ID and make redelivery a no-op; write business state by upserting against external ID fields keyed on Stripe's own identifiers for intents, invoices and subscriptions. Because ordering is not guaranteed, treat an event as a hint rather than a command: before a critical transition, fetch the object's current state from the Stripe API, and never let an older event overwrite a newer state — a small state machine with monotonic transitions is cheap insurance. Outbound, every mutating Stripe call carries an idempotency key derived from the Salesforce record that caused it, so a retried callout cannot charge or refund twice. Finally, plan for the webhooks you never receive: a scheduled job that lists events from the Stripe API since the last processed checkpoint and replays gaps turns a lost event from an incident into a non-event. These are the general rules from our integration patterns guide, applied to the least forgiving domain there is.

STRIPE → SALESFORCE WEBHOOK PIPELINE Stripe events · at-least-once Ingestion endpoint verify signature · ack fast Durable queue dedupe on event ID · retry Salesforce upsert by Stripe ID reconciliation loop — nightly job lists events since last checkpoint · fetches object state · replays gaps
Verify, acknowledge, queue, upsert — and a replay loop for the events that never arrive.

PCI scope: tokens only in Salesforce

If a primary account number ever transits or rests in Salesforce, your org is inside PCI DSS assessment scope — and a CRM full of reports, exports, sandboxes and integration users is a miserable cardholder-data environment to defend. The architecture rule is absolute: card collection happens on Stripe-hosted surfaces, and Salesforce stores only tokens — customer, payment-method and intent identifiers — plus the display metadata Stripe exposes, such as brand, last four digits and expiry. That is enough for support conversations, renewal charges and dunning; nothing about servicing a customer requires the number itself. Qualitatively, keeping collection fully hosted by Stripe puts you in the lightest self-assessment tier, while embedding your own card forms shifts real compliance burden onto you — confirm the exact questionnaire level with your acquirer or QSA rather than assuming.

The leak nobody designs for is human. An agent takes a card number over the phone and types it into a case comment, and Salesforce is now storing cardholder data regardless of your architecture diagrams. Treat this as a control problem, not a training slide: prohibit card numbers in free-text fields as policy, detect pattern violations, and if phone payments are genuinely required, give agents a purpose-built flow — a payment link sent mid-call, or an agent-assisted hosted page — that keeps the digits out of Salesforce entirely.

Reconciliation: from charge to payout to bank to ledger

Finance does not close the month on webhooks. It closes on three layers agreeing: what Salesforce expected (invoices and orders), what Stripe settled (charges, net of fees, refunds and disputes), and what the bank received (one deposit per payout). The layers deliberately do not map one-to-one — a payout bundles many charges into a single net bank line — so reconciliation means exploding each payout into its balance transactions, matching charges back to invoices or orders, and matching the payout total to the bank statement line before anything posts to the ledger.

The cheapest investment in this whole architecture is made at charge-creation time: stamp your Salesforce invoice or order identifier into Stripe metadata on every intent and charge. With it, charge-to-invoice matching is a join; without it, it is fuzzy matching on amounts and dates, forever. The second cheapest is refusing to do the matching in spreadsheets. Our TransactionHub product was built for exactly this seam — it connects the payment gateways customers actually use, Stripe among them, alongside PSD2 open-banking bank feeds, and runs the matching inside Salesforce so the close is a review rather than a hunt. The matching-engine mechanics — rules, tolerances, exception queues — are covered in depth in our guide to bank feed reconciliation in Salesforce.

Subscription billing: one owner for price and term

The most expensive Stripe–Salesforce mistake is not technical. It is two systems both believing they own the subscription — Stripe prorating one way, Salesforce amending another, and finance discovering at renewal that neither matches the contract. The fix is an ownership decision made per sales motion, before any field mapping.

For self-serve products, let Stripe Billing own the subscription: catalog, trials, proration, invoicing and retry logic are what it is built for. Salesforce mirrors subscription state read-only so sales and support see plan, status and renewal date, and expansion signals land in the pipeline — but no Salesforce process writes subscription terms back. For negotiated B2B contracts, invert it: Salesforce CPQ or Revenue Cloud owns the catalog, quote, contract, amendments and co-terming — commercial logic Stripe's catalog was never meant to express — and Stripe becomes payment rails, collecting the finalized amounts against saved payment methods. That flow, from quote through order to cash, is its own discipline; our order-to-cash guide covers the Salesforce side of it.

Companies running both motions should partition ownership explicitly — by product line or customer segment — and record on every subscription which system is authoritative. What does not work is bidirectional sync: two writers on one state converge on neither, and every incident review ends with the same question of who moved last. One owner, one mirror, one direction of truth.

Frequently asked questions

Can Stripe send webhooks directly to Salesforce?

Technically yes — an Apex REST endpoint exposed through a public site can receive Stripe events. It works at low volume, but it ties Stripe's retry schedule to Salesforce availability, runs signature verification in a guest-user context you must harden, and spends API and processing capacity on every retry burst. For anything business-critical we put a thin ingestion service or middleware in front: verify, acknowledge, queue, then deliver to Salesforce at a controlled rate.

Does storing Stripe IDs in Salesforce put us in PCI scope?

Stripe customer, payment-method and intent identifiers are tokens, not cardholder data — storing them alongside display metadata such as card brand, last four digits and expiry is standard practice and keeps Salesforce out of the cardholder-data environment. Scope problems come from primary account numbers entering Salesforce: embedded card forms, or agents typing digits into case fields. Keep collection on Stripe-hosted surfaces and confirm your exact self-assessment level with your acquirer or QSA.

Should subscriptions live in Stripe Billing or in Salesforce?

Pick one owner per sales motion. Self-serve products fit Stripe Billing: it owns the catalog, proration, invoicing and retries, and Salesforce mirrors subscription state read-only. Negotiated B2B contracts fit Salesforce CPQ or Revenue Cloud: quotes, amendments and co-terming live there, and Stripe executes collection on the finalized amounts. What fails is bidirectional ownership — two systems both writing subscription state will drift, and finance inherits the mess.

How do we reconcile Stripe payouts against the bank statement?

A Stripe payout arrives at the bank as one net deposit covering many charges, minus fees, refunds and disputes. Reconciliation means exploding each payout into its balance transactions, matching charges back to Salesforce invoices or orders — cheap if you stamped Salesforce identifiers into Stripe metadata at creation — and matching the payout total to the bank line. Our TransactionHub product automates this matching inside Salesforce, with Stripe among its supported gateways.

What happens if we miss a webhook event?

Stripe retries failed webhook deliveries with backoff over multiple days, so short outages usually self-heal. Design for the case where they do not: run a scheduled job that lists events from the Stripe API since the last processed checkpoint and replays anything missing. Treat webhooks as at-least-once triggers and the Stripe API as the source of truth — a state fetch before critical transitions makes lost or late events a non-issue.

Related reading

Salesforce integration services — how we design and build it TransactionHub and CollectiPro — our Salesforce products Bank feed reconciliation in Salesforce Salesforce integration patterns that survive production Order-to-cash on Salesforce
Need this in your org?

Bring the hard part to a senior Salesforce engineering team.

Talk to the builders about this pattern