In today’s fast-paced digital economy, businesses need a quick and reliable way to accept payments directly from their CRM. Salesforce, the world’s #1 CRM platform, when integrated with Stripe, one of the most popular payment processors, creates a powerful synergy.

This integration allows sales teams to collect payments without leaving Salesforce, automate transaction records, and offer a smooth checkout experience to customers — all in real time.

In this blog, we’ll walk through how to integrate Stripe’s Payment Intents API into Salesforce using Apex, Lightning Web Components (LWC), and Stripe.js. You’ll learn how to:

  • Store API keys securely in Custom Metadata
  • Create Payment Intents in Stripe via Apex
  • Collect card details using Stripe.js in LWC
  • Confirm payments in real-time
  • Add optional enhancements like billing details, redirects, and recurring payments

Why Businesses Need Salesforce–Stripe Integration

  • Faster Payment Processing – No need to switch between platforms; charge customers instantly from Salesforce.
  • Better Customer Experience – Provide a professional checkout flow within your CRM.
  • Centralized Data – Store payment details in Salesforce for future insights and reporting.
  • Automation Ready – Trigger workflows, approvals, or notifications when a payment is processed.

Integration Overview

Our goal was simple:

  1. Accept card payments directly in Salesforce via a custom Lightning Web Component.
  2. Use Stripe’s Payment Intents API to securely handle payments.
  3. Save transaction details in a custom Salesforce object for tracking and analytics.

Overview of the Salesforce–Stripe Payment Flow

Here’s the flow we’ll be building:

  1. Apex securely calls Stripe’s Payment Intents API.
  2. Stripe Secret Key is stored in Custom Metadata.
  3. LWC renders a Stripe.js card input for secure payment collection.
  4. Client-side confirmation with stripe.confirmCardPayment() using the client_secret.
  5. Real-time payment status is shown to the user.

Prerequisites

Before starting, you’ll need:

  • A Salesforce Developer org
  • A Stripe account (free to sign up)
  • LWC – Front-end UI for payment form.
  • Apex – Server-side callouts to Stripe API.
  • Stripe Publishable Key and Secret Key from:

    Stripe Dashboard → Developers → API Keys

  • CSP Trusted Site entry for https://js.stripe.com (Setup → CSP Trusted Sites).
  • Custom Object – Save transaction history in Salesforce.

Check out below screenshot to view Publishable & Secret key in Stripe.

Storing Stripe API Keys Securely

Instead of hardcoding keys in the LWC, we used Custom Metadata (Stripe_Configuration__mdt) to store:

  • Publishable_Key__c – Used in LWC for Stripe.js.
  • Secret_Key__c – Used in Apex for secure API calls.

This ensures credentials are managed centrally and securely.

Payment Flow We Implemented

  1. User enters payment details (amount, currency, billing info) in LWC.
  2. LWC fetches the Stripe Publishable Key from Apex.
  3. Stripe.js securely collects card information and generates a Payment Method.
  4. Apex creates a Payment Intent via Stripe’s API using the secret key.
  5. LWC confirms payment using stripe.confirmCardPayment() with the client secret.
  6. Apex saves transaction details in Stripe_Transaction__c custom object.
  7. UI shows success message & spinner during processing for a smooth experience.

User Experience Highlights

  • Spinner / Loading State – Prevents duplicate clicks and improves UX.
  • Success Toast – Instant confirmation when payment succeeds.
  • Error Handling – Clear messages if card is declined or invalid.

Step-by-Step Implementation Guide

  1. Step 1: Create Custom Metadata Type Stripe_Configuration__mdt with fields:

    • Publishable_Key__c (Text)
    • Secret_Key__c (Text)
    • Webhook_Secret__c (Text)

    Create a record (e.g. Test_Stripe_Configuration) and set test keys.

  2. Step 2: Create Named Credential

    • Label/Name: StripeAPI
    • URL: https://api.stripe.com
    • Store Secret Key in Custom Headers
  3. Step 3: Create Custom Object Stripe_Transaction__c with fields:

    • Amount__c (Number)
    • Currency__c (Text)
    • Payment_Intent_Id__c (Text)
    • Billing_Name__c (Text)
    • Billing_Email__c (Email)
    • Status__c (Text) — optional
  4. Step 4: CSP Trusted Sites

    • Add trusted URL https://js.stripe.com with contexts: Lightning Components, Lightning Locker, Communities.
  5. Step 5: Apex Controller to Create Payment Intent

public with sharing class StripePaymentController {

    // Helper: get secret from custom metadata
    private static String getStripeSecretKey() {
        Stripe_Configuration__mdt config = Stripe_Configuration__mdt.getInstance('Test_Stripe_Configuration');
        if (config == null || String.isEmpty(config.Secret_Key__c)) {
            throw new AuraHandledException('Stripe Secret key is not configured');
        }
        return config.Secret_Key__c;
    }

    // 1) Create Payment Intent and return full JSON string (includes client_secret)
    @AuraEnabled
    public static String createPaymentIntent(
        Decimal amount,
        String currencyCode,
        String billingName,
        String billingEmail
    ) {
        String secretKey = getStripeSecretKey();

        HttpRequest req = new HttpRequest();
        req.setEndpoint('callout:StripeAPI/v1/payment_intents');
        req.setMethod('POST');
        req.setHeader('Authorization', 'Bearer ' + secretKey);
        req.setHeader('Content-Type', 'application/x-www-form-urlencoded');

        // amount converted to cents
        Integer amountInCents = Integer.valueOf((amount == null) ? 0 : (amount.multiply(100)).intValue());
        String body = 'amount=' + String.valueOf(amountInCents)
                    + '&currency=' + EncodingUtil.urlEncode(currencyCode, 'UTF-8')
                    + '&payment_method_types[]=card'
                    + '&description=' + EncodingUtil.urlEncode('Payment from ' + billingName, 'UTF-8')
                    + '&receipt_email=' + EncodingUtil.urlEncode(billingEmail, 'UTF-8')
                    + '&metadata[billing_name]=' + EncodingUtil.urlEncode(billingName, 'UTF-8')
                    + '&metadata[billing_email]=' + EncodingUtil.urlEncode(billingEmail, 'UTF-8');

        req.setBody(body);

        Http http = new Http();
        HttpResponse res = http.send(req);

        if (res.getStatusCode() == 200 || res.getStatusCode() == 201) {
            return res.getBody();
        } else {
            // bubble up stripe error to UI
            throw new AuraHandledException('Stripe Error: ' + res.getBody());
        }
    }

    // 2) Save Stripe transaction record in Salesforce
    @AuraEnabled
    public static void saveStripeTransaction(String amount, String currencyCode, String paymentIntentId, String billingName, String billingEmail) {
        Stripe_Transaction__c tx = new Stripe_Transaction__c();
        tx.Amount__c = (amount == null) ? null : Decimal.valueOf(amount);
        tx.Currency__c = currencyCode;
        tx.Payment_Intent_Id__c = paymentIntentId;
        tx.Billing_Name__c = billingName;
        tx.Billing_Email__c = billingEmail;
        insert tx;
    }

    // 3) Provide publishable key to LWC (cacheable)
    @AuraEnabled(cacheable=true)
    public static String getStripePublishableKey() {
        Stripe_Configuration__mdt config = Stripe_Configuration__mdt.getInstance('Test_Stripe_Configuration');
        if (config == null || String.isEmpty(config.Publishable_Key__c)) {
            throw new AuraHandledException('Stripe Publishable key is not configured');
        }
        return config.Publishable_Key__c;
    }
}

6. Step 6: LWC HTML – Payment Form

    • Form fields for amount, currency, billing name/email, and card input container for Stripe.js.

<template>
    <lightning-card title="Stripe Payment">
        <div class="slds-p-around_medium">
            <lightning-input name="amount" label="Amount" type="number" value={amount} onchange={handleInputChange} required></lightning-input>
            <lightning-input name="currencyCode" label="Currency (e.g., usd)" type="text" value={currencyCode} onchange={handleInputChange} required></lightning-input>
            <lightning-input name="billingName" label="Billing Name" type="text" value={billingName} onchange={handleInputChange} required></lightning-input>
            <lightning-input name="billingEmail" label="Billing Email" type="email" value={billingEmail} onchange={handleInputChange} required></lightning-input>

            <div class="slds-m-top_medium card-element" style="border:1px solid #ccc; padding:12px; border-radius:4px;"></div>

            <div class="slds-m-top_medium">
                <lightning-button variant="brand" label="Pay Now" onclick={handlePay} disabled={isLoading}></lightning-button>
            </div>

            <template if:true={isLoading}>
                <div class="slds-spinner_container slds-m-top_small">
                    <div role="status" class="slds-spinner slds-spinner_medium slds-spinner_brand">
                        <span class="slds-assistive-text">Loading</span>
                        <div class="slds-spinner__dot-a"></div>
                        <div class="slds-spinner__dot-b"></div>
                    </div>
                </div>
            </template>

            <template if:true={paymentStatus}>
                <div class="slds-text-color_success slds-m-top_medium">{paymentStatus}</div>
            </template>

            <template if:true={error}>
                <div class="slds-text-color_error slds-m-top_medium">Error: {error}</div>
            </template>
        </div>
    </lightning-card>
</template>

7. Step 7: LWC JavaScript – Integrating Stripe.js

    • Load Stripe.js dynamically.
    • Fetch publishable key from Apex.
    • Collect card details with Stripe Elements.
    • Call Apex to create Payment Intent.
    • Use stripe.confirmCardPayment() to complete payment.
import { LightningElement, track } from 'lwc';
import { loadScript } from 'lightning/platformResourceLoader';

import createPaymentIntent from '@salesforce/apex/StripePaymentController.createPaymentIntent';
import saveStripeTransaction from '@salesforce/apex/StripePaymentController.saveStripeTransaction';
import getStripePublishableKey from '@salesforce/apex/StripePaymentController.getStripePublishableKey';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

const STRIPE_JS = 'https://js.stripe.com/v3/';

export default class StripePayment extends LightningElement {
    @track amount;
    @track currencyCode = 'usd';
    @track billingName;
    @track billingEmail;

    @track error;
    @track paymentStatus;
    @track isLoading = false;

    stripe;
    card;
    stripeInitialized = false;

    renderedCallback() {
        if (this.stripeInitialized) return;

        // Load Stripe.js and fetch publishable key in parallel
        Promise.all([
            loadScript(this, STRIPE_JS),
            getStripePublishableKey()
        ])
        .then(([_, publishableKey]) => {
            this.stripe = Stripe(publishableKey);
            const elements = this.stripe.elements();
            this.card = elements.create('card', {
                hidePostalCode: true,
                style: {
                    base: {
                        fontSize: '16px',
                        color: '#32325d',
                        '::placeholder': { color: '#aab7c4' }
                    },
                    invalid: { color: '#fa755a', iconColor: '#fa755a' }
                }
            });

    handleInputChange(evt) {
        const { name, value } = evt.target;
        // Keep mapping safe and explicit
        if (name === 'amount') this.amount = value;
        if (name === 'currencyCode') this.currencyCode = value.toLowerCase();
        if (name === 'billingName') this.billingName = value;
        if (name === 'billingEmail') this.billingEmail = value;
    }

    async handlePay() {
        this.error = null;
        this.paymentStatus = null;
        this.isLoading = true;

        // Basic validation
        if (!this.amount || !this.currencyCode || !this.billingName || !this.billingEmail) {
            this.error = 'Please fill all required fields.';
            this.isLoading = false;
            return;
        }

        try {
            // 1) Create PaymentIntent on server
            const response = await createPaymentIntent({
                amount: parseFloat(this.amount),
                currencyCode: this.currencyCode,
                billingName: this.billingName,
                billingEmail: this.billingEmail
            });

           
            if (confirmResult.error) {
                // show Stripe error
                this.error = confirmResult.error.message;
                this.dispatchEvent(new ShowToastEvent({
                    title: 'Payment failed',
                    message: this.error,
                    variant: 'error'
                }));
            } else if (confirmResult.paymentIntent && confirmResult.paymentIntent.status === 'succeeded') {
                const paymentIntentId = confirmResult.paymentIntent.id;
                // 3) Save transaction in Salesforce
                await saveStripeTransaction({
                    amount: this.amount.toString(),
                    currencyCode: this.currencyCode,
                    paymentIntentId: paymentIntentId,
                    billingName: this.billingName,
                    billingEmail: this.billingEmail
                });

                this.paymentStatus = '💰 Payment successful!';
                this.dispatchEvent(new ShowToastEvent({
                    title: 'Success',
                    message: 'Payment completed and saved',
                    variant: 'success'
                }));
            } else {
                this.error = 'Payment did not complete.';
            }
        } catch (err) {
            console.error('Payment error', err);
            this.error = err?.body?.message || err.message || String(err);
            this.dispatchEvent(new ShowToastEvent({
                title: 'Error',
                message: this.error,
                variant: 'error'
            }));
        } finally {
            this.isLoading = false;
        }
    }
}

8. Step 8: Add stripe.Payment.js-meta.xmlfile

<?xml version="1.0" encoding="utf-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>64.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__RecordPage</target>
        <target>lightning__AppPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>

9. Step 9: Test the integration

 

    1. Open a Lightning App Page or Record Page with the LWC.
    2. Enter test values (Amount, Currency USD, Billing Name/Email).
    3. For card details in the Stripe card input use below Stripe test cards with any future expiry and CVC:
💳 Card Number📄 Description
4242 4242 4242 4242Successful payment
4000 0025 0000 3155Requires authentication
4000 0000 0000 9995Payment declined


Final Result

You’ve successfully built a fully functional Stripe payment flow right inside Salesforce — featuring secure API calls, real-time payment confirmation, and transaction logging for complete visibility.

Watch the demo video below to see the integration in action:

Conclusion

This integration brings secure, frictionless payments directly into the core of Salesforce — no external tools, no manual workarounds. By keeping API keys safe on the server, following Stripe’s SCA-ready Payment Intents best practices, and logging transactions directly in Salesforce, you ensure every payment is compliant, trackable, and automation-ready.

Whether you’re managing subscriptions, handling one-time purchases, or accepting donations, this setup keeps your data perfectly in sync, boosts operational efficiency, and delivers a seamless customer experience — all without leaving your CRM.

With Salesforce and Stripe working together, your CRM transforms into a complete sales, payment, and revenue powerhouse.

Thinking about implementing a similar integration? Now’s the perfect time to bridge the gap between your CRM and payments — and take your customer journey to the next level. Contact The Pinq Clouds today!