A Stripe CRM integration keeps billing facts from Stripe (customers, subscriptions, invoices, payments, refunds and disputes) visible on the matching records in Salesforce, HubSpot or your own CRM. You can install a vendor app, wire up a no-code connector or build a webhook-driven sync. Whichever you choose, decide which system owns each field, store Stripe IDs on CRM records and make every write safe to repeat.
This guide explains what people usually mean by a "Stripe connector", what Stripe's Salesforce app and HubSpot's Stripe integration do according to each vendor's own documentation, and how to map Stripe objects to CRM records. It then covers source of truth and sync direction, IDs and metadata, an idempotent webhook sync with Node.js code, backfills, deletes and merges, revenue fields, least-privilege keys and a decision table. Checkout, keys and the wider build are in our Stripe integration guide; webhook internals are in Stripe webhooks best practices. Here we stay on the CRM side.
What is a Stripe connector, and what does a Stripe CRM integration involve?
A Stripe connector is usually a prebuilt integration, not your own code: an app in a CRM marketplace, a step in a no-code automation tool, or a data pipeline that copies Stripe records somewhere else. A Stripe CRM integration is the narrower job of keeping Stripe's billing records and your CRM's customer records describing the same customer.
The word "connector" hides three very different products. A marketplace app is installed inside the CRM and maps Stripe objects to CRM objects using a model its vendor chose. A no-code automation reacts to one Stripe event at a time, such as "when a payment succeeds, update this contact". A built integration is code you own, usually a webhook endpoint plus a worker that writes to the CRM's API. All three can put a payment on a contact record. They differ in what happens when an event arrives twice, arrives late, refers to a customer the CRM does not know yet, or fails halfway through.
Before choosing, write down what the CRM is for in this integration. Most teams want one or more of these outcomes:
- Sales and account managers see billing state on the account: plan, renewal date, open invoices, failed payments and lifetime value, without logging in to Stripe.
- Revenue reporting in the CRM joins pipeline data to what was actually billed and collected.
- Workflows fire on billing events: a failed renewal creates a task for the account owner, a dispute alerts finance, a cancelled subscription starts a win-back sequence.
- Quote-to-cash runs from the CRM: a closed deal creates the Stripe customer and subscription, so nobody re-keys prices.
The first three are mostly one-way flows from Stripe into the CRM. The fourth reverses the direction for a few objects, which is where most of the design risk sits. In-person payments follow the same pattern: a card-present payment from a reader is an ordinary PaymentIntent once it reaches Stripe, so the question "how do I connect a payment terminal to a CRM" is answered by the same webhook sync described below, with order and store IDs in metadata. Our Stripe Terminal guide covers the reader side.
What are the ways to connect Stripe to a CRM?
There are three routes: a vendor app inside the CRM, a general-purpose automation or integration platform, or a custom sync driven by Stripe webhooks. Many mature setups end up as a hybrid, with an app for the standard objects and a small amount of code for whatever the app cannot reach.
Vendor apps from Stripe or the CRM
Both Salesforce and HubSpot have Stripe integrations described in their vendors' own documentation, covered in the next section. Their strength is that the mapping, the user interface and the upgrade path belong to someone else. Their limit is the same thing: the data model, which objects sync in which direction, and how failures surface are decided by the app. If your CRM uses custom objects for accounts, locations or contracts, check whether the app can write to them before you commit.
Integration platforms and no-code automation
Integration platform as a service (iPaaS) and workflow automation tools offer Stripe triggers and CRM actions you join in a visual editor. They are a reasonable choice for notifications and simple field updates at modest volume. Before relying on one for anything that moves money or grants access, find out four things: whether it deduplicates events by Stripe event ID, whether it retries failed steps and shows you the failures, whether it can re-read the latest object rather than trusting the event payload, and which Stripe key permissions it asks for. If the answer to the last is "a full secret key", treat that as a security decision, not a setup step.
A custom webhook-driven sync
A custom sync is a webhook endpoint that verifies and records Stripe events, a worker that re-fetches each object and writes it to the CRM by Stripe ID, a backfill script for history and a nightly reconciliation job. It is more work to build and you own its monitoring. In exchange you control the mapping, can write to any object the CRM's API exposes, and can test every failure path. It is the usual answer for in-house CRMs, Stripe Connect platforms that need per-account records, and businesses where a billing event drives something that must happen exactly once.
What do the Stripe Salesforce and HubSpot integrations do?
Stripe documents an app for Salesforce Platform that brings Stripe API actions, events and billing objects into Salesforce Flow and Apex. HubSpot documents two separate uses of Stripe: as a payment processor for HubSpot's own payment tools, and as a data sync that brings Stripe records into HubSpot. The descriptions below come from each vendor's documentation as read in October 2026; check the current pages before you plan around them.
The Stripe app for Salesforce Platform
According to Stripe's documentation for the Stripe app for Salesforce Platform, the app works as an integration builder inside Salesforce. You build with Salesforce Flow Builder and Apex, using prebuilt Stripe API actions, and you can subscribe to Stripe events and map them to Salesforce objects and fields you choose. The same page lists support for the Stripe Billing data model as custom objects with a default mapping to Sales Cloud objects, a one-click billing data sync that can run in real time or on a schedule, configurable bidirectional synchronisation, and templates that create or update Stripe subscriptions when orders are activated or quotes are converted.
Three practical points follow from that description. First, it is a toolkit as much as a connector: the Billing sync is close to turnkey, but anything beyond it is a Flow or Apex build your Salesforce team owns. Second, Stripe notes the app works best on core clouds such as Sales, Revenue, Service, Commerce and Experience Cloud, so check fit if your org runs elsewhere. Third, optional extensions add Salesforce Billing payments and subscription flows; read the installation and event configuration guides linked from the overview before you scope the work.
HubSpot's Stripe integration
HubSpot's knowledge base article on using Stripe with HubSpot separates two things that are easy to confuse. Connecting Stripe as a payment processor lets you collect payments with HubSpot's own payment tools. The Stripe data sync brings Stripe data into HubSpot without setting up payment processing. For the data sync, the article lists Stripe customers syncing with HubSpot contacts in both directions, while invoices and products sync one way from Stripe into HubSpot invoices and products, and subscriptions and payment transactions sync one way from Stripe into custom objects.
That mapping shapes your design. Customers are the only two-way object, so a contact edit in HubSpot can reach the Stripe customer, which makes field ownership (covered below) a live question from day one. Subscriptions and payments land as custom objects, so reports and workflows need to be built against those objects rather than deals. Refunds and disputes are handled in HubSpot's payment tools for payments processed through HubSpot; if you need dispute tasks for payments taken elsewhere in Stripe, check whether the sync covers them or plan a small webhook handler for that gap. Plan and seat requirements also apply; HubSpot's page lists them.
What to check before installing any connector
- Objects and direction. List every Stripe object you need in the CRM and confirm, from the vendor's page, which ones sync and in which direction.
- Matching rule. How does the connector decide that a Stripe customer and a CRM contact are the same person: by email, by stored ID, or by both? Email matching breaks on shared billing inboxes and changed addresses.
- History. Does it import existing records, or only changes from the day you connect?
- Failures. Where do sync errors appear, who is notified, and can you replay them?
- Permissions. Which Stripe key or OAuth scope does it require, and can you limit it?
How do Stripe objects map to CRM records?
Map the Stripe Customer to the CRM record that pays (a company or account in B2B, a contact in B2C), then hang subscriptions, invoices and payments off that record as child objects. Refunds and disputes belong on the payment they reverse, with a task or alert for the person who owns the account.
Customers, accounts and contacts
A Stripe Customer is a billing identity: an email, a name, addresses, tax details and payment methods. A CRM separates the organisation (account or company) from the people (contacts). In B2B, link the Stripe Customer to the company and store the billing email as an attribute, because the person who pays an invoice is often not your champion. One company can have several Stripe Customers (one per subsidiary, currency or legal entity), so model the link as one-to-many from the company. In B2C, a contact usually maps one-to-one to a Stripe Customer.
Subscriptions, invoices and payments
Give subscriptions their own CRM object or custom object with the Stripe subscription ID, status, price or product, current period end, cancel-at-period-end flag and the Stripe customer ID as the parent link. Do not overwrite a deal's amount with subscription data: a deal records what was sold, a subscription records what is being billed, and those diverge after the first upgrade. Invoices carry number, status, amount due, amount paid, due date and the hosted invoice URL, which is useful for account managers chasing payment. Payments (PaymentIntents or Charges) carry amount, currency, status, created date and failure reason. Subscription lifecycle events are covered in our Stripe subscription webhooks guide, and Stripe Billing work in more depth on its own page.
Refunds and disputes
Refunds reduce what you collected, so store them against the original payment and recompute any lifetime value field from Stripe rather than subtracting in the CRM. Disputes are time-bound: Stripe sets a deadline for submitting evidence, so the useful CRM action is a task assigned to the account owner or finance with the dispute reason, amount and due date, not just a field update. When the dispute closes, update the task with the outcome.
Which revenue and reporting fields belong in the CRM?
Sync facts and compute summaries in one place. Good CRM fields are ones a salesperson acts on: current plan, subscription status, next renewal date, open invoice amount, last payment date, last failed payment date and lifetime amount collected net of refunds. Recurring revenue figures are trickier: discounts, proration, trials, multiple currencies and usage-based prices all change the number. Pick one definition, compute it in one service (or take it from Stripe's own reporting), and write it to the CRM as a read-only field with the date it was computed. Two teams calculating revenue in two systems produces two numbers, and the CRM's will usually be the wrong one. Store amounts in the smallest currency unit with the currency code alongside, exactly as Stripe does, and convert only for display.
Which system is the source of truth, and which way should data sync?
Stripe should own anything that affects billing: billing contact details, prices, subscription state, invoices and money movements. The CRM should own the commercial relationship: owner, segment, deal stage, notes and activities. Each field has exactly one owner, and the other system holds a read-only copy.
Two-way sync is where integrations go wrong. If a billing email can be edited in both Stripe and the CRM, and both systems sync changes to each other, a slow event can overwrite a newer edit, and two near-simultaneous edits can bounce back and forth. The fix is to make the CRM a requester rather than a writer for Stripe-owned fields. If an account manager needs to change the billing email, the CRM calls your service, your service updates Stripe, and the change returns to the CRM through the normal Stripe-to-CRM sync. The CRM field stays read-only, and there is never a question about which value is current.
The genuine CRM-to-Stripe flows are few and usually event-shaped: create a Stripe customer when a deal closes, create a subscription from an approved quote, or update metadata with the CRM record ID. Treat each as a command with an idempotency key derived from the CRM record (for example, the deal ID plus the action), so a retried workflow cannot create a second customer or subscription. The Stripe integration guide explains how Stripe's idempotency keys work.
How do you sync Stripe customers to a CRM using IDs and metadata?
Store the Stripe customer ID on the CRM record as a unique field, and store the CRM record ID in the Stripe customer's metadata. Match on those IDs, never on email or name, and every later sync becomes a lookup instead of a guess.
Stripe's metadata documentation describes metadata as key-value pairs for your own reference, with up to 50 keys per object, keys up to 40 characters and values up to 500 characters. It recommends storing an external system's ID in metadata when you need more space than that, and warns never to store sensitive information such as bank account or card details. A practical convention is `crm_account_id` and `crm_system` on the Customer, plus your own `order_id` or `deal_id` on PaymentIntents and Subscriptions.
Two details from the same page matter for CRM work. Metadata does not copy automatically between related objects, with a few listed exceptions: PaymentIntent metadata is copied to the Charge it creates as a one-time snapshot, and a subscription's metadata is copied onto the invoices it generates under the invoice's subscription details. So if the CRM needs the deal ID on an invoice, either set it on the subscription before invoices are created, or look it up through the customer. And when you create a Checkout Session, metadata set on the session is not the same as metadata on the underlying PaymentIntent or Subscription; use the nested `payment_intent_data.metadata` or `subscription_data.metadata` parameters if the CRM will read those objects.
Matching existing records is the one place email is acceptable, and only once. When you first connect a CRM that already holds customers, run a matching job: exact Stripe ID where one exists, then exact email to a single CRM record, then a review queue for everything else (no match, several matches, shared inboxes such as accounts@ or billing@). Write the result into both systems, and from then on only IDs are used. Skipping the review queue puts invoices on the wrong account.
How do you build an idempotent, webhook-driven Stripe to CRM sync?
Verify each Stripe event, record its event ID under a unique constraint, return 2xx, and let a worker re-fetch the object from Stripe and upsert it into the CRM by Stripe ID. Duplicates are skipped by the event table, out-of-order delivery is neutralised by re-fetching, and the CRM write is safe to repeat.
Stripe's webhook documentation sets out the constraints this design answers. Endpoints might occasionally receive the same event more than once, so log processed event IDs and skip repeats. Stripe does not guarantee delivery in the order events were generated, and advises using the API to retrieve missing or current objects rather than relying on order or on the event's `created` timestamp. Endpoints must return a 2xx status quickly, before any complex logic that could time out, and Stripe recommends processing events through an asynchronous queue so a spike in deliveries (for example, when many subscriptions renew at once) does not overwhelm your servers. In live mode, Stripe retries failed deliveries for up to three days with exponential backoff. Signature verification needs the raw request body, untouched by any JSON parser.
The code below is a minimal, working shape of that design in Node.js with Express and PostgreSQL. The `crm` adapter stands in for whichever CRM API you use; most CRM APIs offer a way to create-or-update a record by a unique external ID field, and that is the call it should wrap.
// Stripe -> CRM sync, part 1: the webhook endpoint (Node.js, Express, PostgreSQL).
// It verifies, records each event once and returns 2xx. No CRM call happens here.
import express from "express";
import Stripe from "stripe";
import pg from "pg";
import { crm } from "./crm-adapter.js"; // your adapter over the CRM's API
import { toCrmFields } from "./mapping.js"; // your Stripe -> CRM field mapping
// A restricted key (rk_...) with Read on Customers, Subscriptions, Invoices,
// Charges and Disputes. The sync never needs to write to Stripe.
const stripe = new Stripe(process.env.STRIPE_SYNC_KEY);
const db = new pg.Pool();
const app = express();
// Table: stripe_events(id text primary key, type text, object_type text,
// object_id text, status text, attempts int default 0, last_error text,
// received_at timestamptz default now())
app.post(
"/webhooks/stripe-crm",
express.raw({ type: "application/json" }), // raw body, or verification fails
async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers["stripe-signature"],
process.env.STRIPE_CRM_WEBHOOK_SECRET,
);
} catch {
return res.status(400).send("Invalid signature");
}
const obj = event.data.object;
// The primary key makes a redelivered event a no-op.
await db.query(
`INSERT INTO stripe_events (id, type, object_type, object_id, status)
VALUES ($1, $2, $3, $4, 'pending')
ON CONFLICT (id) DO NOTHING`,
[event.id, event.type, obj.object, obj.id],
);
res.sendStatus(200);
},
);
// Part 2: the worker. Run it on a schedule or as a queue consumer.
const fetchLatest = {
customer: (id) => stripe.customers.retrieve(id),
subscription: (id) => stripe.subscriptions.retrieve(id),
invoice: (id) => stripe.invoices.retrieve(id),
charge: (id) => stripe.charges.retrieve(id),
dispute: (id) => stripe.disputes.retrieve(id),
};
export async function processPendingEvents() {
const { rows } = await db.query(
`UPDATE stripe_events SET status = 'processing'
WHERE id IN (
SELECT id FROM stripe_events
WHERE status = 'pending' AND attempts < 10
ORDER BY received_at LIMIT 50
FOR UPDATE SKIP LOCKED)
RETURNING id, object_type, object_id`,
);
for (const row of rows) {
try {
const load = fetchLatest[row.object_type];
if (load) {
// Re-fetch current state: late or out-of-order events cannot
// overwrite newer data with older data.
const latest = await load(row.object_id);
if (latest.deleted) {
await crm.markStripeDeleted(row.object_type, row.object_id);
} else {
// Upsert keyed on the Stripe ID: running it twice changes nothing.
await crm.upsertByExternalId(
row.object_type,
latest.id,
toCrmFields(row.object_type, latest),
);
}
}
await db.query("UPDATE stripe_events SET status = 'done' WHERE id = $1", [row.id]);
} catch (err) {
await db.query(
`UPDATE stripe_events
SET status = 'pending', attempts = attempts + 1, last_error = $2
WHERE id = $1`,
[row.id, String(err.message).slice(0, 500)],
);
}
}
}A few choices in that code are deliberate. The endpoint subscribes only to the events the CRM needs (Stripe advises against listening to all events), which for this mapping means customer, subscription, invoice, charge refund and dispute events. The worker claims rows with `FOR UPDATE SKIP LOCKED` so two workers never process the same event. It handles deleted customers explicitly, because retrieving a deleted Customer returns a stub marked `deleted` rather than an error. And it caps attempts, so a record the CRM keeps rejecting (a validation rule, a missing required field) stops retrying and shows up in a dead-letter query instead of looping forever. Alert on that query.
If you run a Stripe Connect platform and need records per connected account, register a destination for connected-account events as well as your own, and include the account ID in both the event table and the CRM's external ID, because object IDs are only meaningful within their account. Our webhook best practices guide goes deeper on ordering and recovery.
How do you backfill historical Stripe data into a CRM?
Start the webhook listener first, note the time, then page through each Stripe list endpoint for objects created up to that time and upsert them into the CRM by Stripe ID. Because both the backfill and the live sync upsert by the same ID, the overlap is harmless and nothing falls into a gap.
Stripe's libraries handle paging for you. According to Stripe's auto-pagination reference, in Node 10 and later you iterate a list call in a `for await` loop and the library fetches subsequent pages automatically; older Node versions use `autoPagingEach`. The script below backfills customers, then subscriptions, then invoices, so child records can link to a parent that already exists in the CRM.
// Stripe -> CRM sync, part 3: a one-off backfill with auto-pagination.
// Start the webhook endpoint first (T0), then run this for everything up to T0.
import Stripe from "stripe";
import { crm } from "./crm-adapter.js";
import { toCrmFields } from "./mapping.js";
const stripe = new Stripe(process.env.STRIPE_SYNC_KEY); // same read-only restricted key
const listenerStartedAt = Math.floor(Date.parse(process.env.LISTENER_STARTED_AT) / 1000);
async function backfill(type, list, extra = {}) {
let count = 0;
let lastId = null;
// In Node 10+, stripe-node auto-paginates any list call inside for await.
for await (const obj of list({ limit: 100, created: { lte: listenerStartedAt }, ...extra })) {
await crm.upsertByExternalId(type, obj.id, toCrmFields(type, obj));
count += 1;
lastId = obj.id;
if (count % 500 === 0) {
// Log a checkpoint. Lists run newest first; to resume after a crash,
// pass starting_after: lastId in `extra`.
console.log(`${type}: ${count} upserted, last ${lastId}`);
}
}
console.log(`${type}: done, ${count} upserted`);
}
// Parents before children, so subscription and invoice records can link
// to a customer record that already exists in the CRM.
await backfill("customer", (p) => stripe.customers.list(p));
await backfill("subscription", (p) => stripe.subscriptions.list(p), { status: "all" });
await backfill("invoice", (p) => stripe.invoices.list(p));Practical notes from running backfills:
- Subscriptions need `status: "all"`. By default the list endpoint leaves out cancelled subscriptions, and churned customers are exactly the history sales and finance want to see.
- Lists come back newest first. Checkpoint the last ID you processed and resume with `starting_after` after a crash, rather than starting again.
- Respect both rate limits. Stripe and every CRM limit request rates. CRM limits are usually the tighter of the two, so batch CRM writes where the API allows it and back off on rate-limit responses.
- Do a dry run. Run the mapping against a sample into a sandbox CRM or a file first, and have someone from sales operations check twenty records by hand.
After the backfill, keep a nightly reconciliation job. It lists objects updated or created in the last day or two, compares them with the CRM by Stripe ID and queues any differences through the same worker. It catches the rare event your endpoint never processed, as well as CRM-side edits to read-only fields that someone made with an admin account.
How do you handle deletes, merges and corrections?
Never cascade a delete automatically between Stripe and a CRM. Mark the counterpart record as deleted or detached and let a person decide. For merges, the CRM record that survives must inherit every Stripe ID from the records merged into it, or future events will find nothing to update.
Deletes in Stripe. Deleting a Stripe Customer cancels its active subscriptions and makes the customer unusable for new charges. Its past invoices and payments still exist. In the CRM, set a `stripe_status` of deleted and keep the record and its billing history; the relationship may still matter commercially, and finance may need the history.
Deletes in the CRM. A salesperson deleting a duplicate contact should not delete a paying Stripe customer. If your CRM sends deletion webhooks, route them to a review queue. If it does not, your nightly reconciliation will find Stripe customers whose CRM ID no longer resolves; queue those for review as well.
Merges in the CRM. CRMs let users merge duplicate records, and the merged-away record's ID usually stops resolving. If both records held a Stripe customer ID, the surviving record must hold both, which is why the link should be one-to-many. Listen for the CRM's merge event if it has one, or detect orphaned IDs during reconciliation, then update the `crm_account_id` metadata on each affected Stripe customer.
Duplicates in Stripe. Stripe has no merge operation for customers. When the same buyer has two Stripe customers, link both to the one CRM record, move future billing to the customer you keep, and leave the other with its history. Do not try to move payments between customers to tidy up.
How do you secure a Stripe CRM integration?
Give every sync component its own restricted API key with only the permissions it needs, usually read-only for a Stripe-to-CRM sync. Verify webhook signatures, keep card and bank data out of metadata and the CRM, and limit who in the CRM can see billing fields.
Stripe's restricted API keys documentation explains that a restricted key (prefixed `rk_live_` or `rk_test_`) can do only what you allow: each resource is set to None, Read or Write, and write permission implies read. Stripe recommends restricted keys over unrestricted secret keys wherever possible, recommends a separate key per service so one compromised service does not expose the rest, and describes a migration path: create the key in a sandbox, test, read the key's request logs for permission errors, then create the live key with the same permissions. That fits CRM work well:
- The Stripe-to-CRM sync worker needs Read on Customers, Subscriptions, Invoices, Charges and Disputes, and nothing else.
- The CRM-to-Stripe command service (if you have one) needs Write on Customers and Subscriptions, and lives behind your own authentication, not in a CRM workflow step.
- A third-party connector gets its own restricted key, or an OAuth connection with the narrowest scope it supports, so revoking it does not affect anything else.
- Webhook signing secrets live in a secrets manager and are rotated on a schedule; the endpoint verifies every request before reading it.
On the CRM side, the integration user should have access to only the objects it writes, and billing fields should be visible only to roles that need them. Payment method details, card data and bank account numbers never belong in the CRM. Stripe's hosted invoice URL is a reasonable thing to show an account manager; a card's last four digits rarely is. If the CRM syncs onward to other tools, check which billing fields go with it.
Connector or custom build: how do you decide?
Use a vendor app when your data model matches its model and the objects you need sync in the direction you need. Use custom code when you need custom objects, Stripe Connect accounts, an in-house CRM, or billing events that trigger actions which must happen exactly once.
Questions that usually settle it:
- What happens if an event is processed twice or not at all? If the answer is "a field is stale until tomorrow", a connector is fine. If it is "a customer is charged, provisioned or chased twice", own the code.
- Does the connector write to the objects your team actually uses? If sales works from custom objects the app cannot reach, the app's data will sit unused.
- Who will maintain it? A Flow-based Salesforce build needs a Salesforce admin or developer; a custom service needs an engineer and an alerting channel. Pick the option someone on your team can own.
- How many Stripe accounts are involved? Connect platforms and multi-entity businesses often outgrow single-account connectors.
From here, this article is about Cevrynt
How Cevrynt approaches Stripe CRM integrations
Cevrynt, Inc. is a Stripe partner in the Stripe Partner Ecosystem, and our engineering services team builds Stripe integrations inside clients' own repositories and Stripe accounts. The work is led by Arindam Pradhan, a Stripe Certified Professional (Implementation Architect); Anupam Pradhan and Subhadip Giri each hold the Stripe Certified Professional (Payments Architect) certification. The team's delivery record, including work as GreenBorn Infotech, covers more than 300 projects and more than 50 CRM deployments.
On a Stripe and CRM project we start with the ownership matrix and the matching rules, then build the pieces this guide describes: verified, idempotent webhook handling, a worker that writes by Stripe ID, a backfill, nightly reconciliation and restricted keys per service, tested in Stripe test mode with duplicate, late and failed events. That work sits within our Stripe payments services. For alternative lenders and ISOs, our lending ops automation service connects the CRM a team already uses to the rest of its stack with webhooks instead of copy-paste. Every project is scoped and priced in writing after a technical review.
If your Stripe data and your CRM disagree, or you are choosing between a connector and a build, tell us what you are working with and we will reply with the questions we need answered first.
