Decide first where the card gets typed: on Stripe’s hosted Checkout page or inside its Payment Element, never in a form of your own. That one choice keeps card numbers off your servers. How to integrate Stripe correctly comes down to 7 parts: the business account, the keys, card entry, a server-set price, a verified webhook, a pinned API version, and separate test and live setups.

How to integrate Stripe: the seven parts of a correct integration

A correct Stripe integration has 7 parts: an activated business account, keys in the right places, card entry on a Stripe surface, a price decided on the server, a verified webhook that grants access, a pinned API version, and separate test and live setups. A successful test payment proves none of them.

A Stripe integration is the code and settings that let your app create a payment on Stripe and then learn, reliably, that the payment happened. A Stripe payment gateway integration can do the first half well and the second half loosely, and the second half is where, as I read it, money and access go wrong. Integrating Stripe is one piece of the wider SaaS billing process best practices; this page covers the integration itself, in the order I’d build or review it.

The middle column below is my reading of what AI builders tend to produce, not a measured count. The checks column is what tells the two apart, and the section on checking your own integration turns each check into a step you can run in a sandbox.

PartWhat correct looks likeWhat a builder often wires insteadThe check that tells them apart
1. Business accountVerified in the Dashboard, so live mode works; public business details and the statement descriptor filled inA sandbox that was never verified, or an account a contractor opened in their own nameLive mode loads, and the account belongs to the business
2. KeysPublishable key (pk_) in the browser; the server key only on the server, a restricted key (rk_) wherever Stripe’s guidance fitsA secret key in client code or committed to the repoThe built client bundle holds no sk_ or rk_ value
3. Card entryCheckout, a Payment Link or the Payment Element, so Stripe collects the cardA card form of your own that posts to your endpointNo field on your domain accepts a card number
4. PriceThe server picks the amount from a Price id or your own price tableAn amount the browser sendsEditing the amount in the request changes nothing
5. WebhookSignature checked against the raw body, each event handled once, and this handler grants or removes accessAccess granted when the success page loadsAccess still arrives when the customer closes the tab before the redirect
6. API versionSet in code and on the webhook endpointNothing set anywhereThe two values match
7. Test and liveEach environment has its own keys, webhook secrets and product idsTest ids or test keys left in productionProduction config holds only live values

Stripe’s API keys page is plain about row 2: only publishable keys are safe to expose outside your backend, and it recommends restricted keys for most use cases because a secret key cannot have its permissions limited. Read top to bottom, the table is a Stripe payment integration guide for review; for the code behind each row, Stripe’s own documentation is the development guide for payment processing, and each section below links the page it relies on. My working rule is that, at the size of a first SaaS, Stripe integration best practices are these seven parts and nothing more exotic, built or reviewed in the table’s order. If you need to integrate a Stripe payment gateway into an app that already works, start at the top row and stop when every check passes.

Stripe offers 3 ways to take a card on a website: Payment Links with no code, Checkout as a Stripe-hosted or embedded page your server opens, and Elements inside your own page. All three keep card entry on Stripe’s side. For a first SaaS integration, I’d use Checkout.

Stripe’s integration options page lays the choices out as five columns, from “No code required” to “Most coding”. When you integrate Stripe into a website, the decision is how much of the payment page you want to own.

SurfaceCode you writeWhere the card is typedWhat you give upFits when
Payment LinksNone (“No code required”)A Stripe-hosted pageLimited customization; the app still needs a webhook to unlock anythingOne product, or a test before launch
Checkout, hosted or embeddedYour server creates a Checkout Session (“Low coding”)A Stripe-hosted page, or Stripe’s form embedded on your siteLimited customizationA SaaS subscription
Elements with Checkout Sessions”More coding”The Payment Element on your own pageMore to get right in your own pageYou need the form inside your own page
Advanced integration (Elements with PaymentIntents)“Most coding”The Payment Element on your own pageDiscount logic, tax calculation and currency conversion become your codeYou want to own every part of checkout

What Stripe calls an advanced integration is the Elements path with Payment Intents handled by your own code. Stripe’s online payments page says that if you use PaymentIntents, “you must manually build equivalent features in your code, including discount logic, tax calculation, and currency conversion.” A first SaaS rarely needs that. Integrating Stripe into a website through a Payment Link is real too, and it is the quickest of the three, but if the app must unlock a plan or a feature after the payment, a webhook is still needed: Stripe’s fulfillment guide says it applies to Payment Links as well, because “Payment Links use Checkout”. The rest of this page assumes Checkout or Elements.

Why it matters: what an AI builder wires, and the half it skips

A generated integration usually reaches a successful test payment, and that proves the happy path and nothing else. The tutorial-style Stripe integration example ends at that first green checkmark, which is the point this page starts from.

Across the apps I audited, 10 of the 21 third-party apps trusted the client: the server accepted whatever the browser asserted. Those 21 were the 11 public apps and the 10 held-out apps I audited in June and July 2026, the second group audited blind, and they are a selected set, not a random sample or a rate for AI-built apps in general. At checkout, trusting the client means a price the customer can edit; that is my reading of the pattern, not a separate count. In the same audits, only 1 of the 21 third-party apps, a healthcare FHIR hub, was credited with a real test suite, and even it skipped sign-up, login and the payment webhook. Three of the audited apps had a real secret permanently in git history: a webhook signing secret, a live AI-provider key, and a Stripe test key with its webhook secret.

Three gaps follow from that, as I see it:

What the generated integration doesWhat is missingWhat it costs you
Takes the price from the browserA price the server looks upRevenue on every order someone edits
Grants access when the success page loadsAccess granted from a payment the server confirmedThe failure modes in the six-ways article’s success-page point, linked below
Never handles renewals or cancellationsThe subscription eventsAccess that outlives the payment

Picture a founder’s app, built on an AI builder, that sells a subscription through Stripe Checkout, where the only code that unlocks the plan runs on the success page. A customer pays and loses the connection before that page loads; Stripe records the payment, and the app never unlocks the plan. Stripe’s own fulfillment page warns about exactly this: “a customer can pay successfully and then lose their internet connection before your landing page loads,” and it says webhooks are required if you sell subscriptions.

The lesson I take from it: a payment Stripe recorded and the app never saw is still a customer you charged, and the webhook is what makes the two records agree. Some builders document the success-page path in their own guides; the server-side section below quotes one. Why a redirect fails as proof in both directions is the success-page point of six ways a vibe-coded checkout leaks money, along with the other leaks it lists and a ten-minute check. The seven conditions to meet before the first real card are in before you turn on payments. If nobody on the team can do this work, what the job contains is covered in hiring someone to add payments to your app.

How it works: the account, the card form, the server, the version and the framework

Five pieces, in the order I’d build them: the account, where the card is typed, what the server owns, the API version, and the framework details.

How to use Stripe for business: the account setup an integration assumes

A Stripe account set up for an integration has 7 settings in place before any code runs: a verified business with a payout bank account, public business details including the statement descriptor, two-factor authentication on every login, products and prices in both modes, Dashboard-managed payment methods, customer emails switched on, and a tax decision.

What Stripe does for an app is take the card or wallet payment and then send funds from your available balance to your bank account as payouts. Getting started with Stripe happens in a sandbox; live mode waits until you verify your business and complete its activation requirements. Most of how to use Stripe well is in the Dashboard rather than the code, and the Stripe settings below are the ones an integration quietly depends on. Stripe’s account setup guide covers the verification itself.

  1. 01 Verify the business in the Dashboard and add a payout bank account in Payout settings, so live keys can take real payments
  2. 02 Fill in the public business details customers see: business name, website, support email and phone, support site URL, and statement descriptor text
  3. 03 Turn on two-factor authentication for every login, ideally with a passkey or security key, and give contractors team-member access rather than the owner's login
  4. 04 Create products and prices in the sandbox, then again in live mode, because objects in one mode are not available in the other
  5. 05 Manage payment methods in the Dashboard with dynamic payment methods instead of listing them in code
  6. 06 Switch on customer emails for successful payments and refunds, and for failed subscription payments
  7. 07 Decide on tax settings, even if the decision for now is to wait

The statement descriptor matters more than it looks: Stripe’s guide says that if a customer can’t recognize one of your payments, they might dispute it. Stripe recommends passkeys or security keys for two-factor authentication and treats SMS as a last resort. The Stripe setup work is the same when a builder wires the code. Lovable’s guide for connecting your own Stripe account says business verification, payouts, taxes and disputes all happen in Stripe. Base44 sets the integration up in a test environment first and creates live API keys once the Stripe account finishes activating.

Payment Element and hosted fields: keeping card entry out of your own form

The Payment Element is Stripe’s embeddable card and wallet form, served by Stripe inside an iframe on your page. Card numbers travel from the browser to Stripe without passing through your server, which is what shrinks a small team’s PCI DSS obligations.

Stripe describes the Payment Element as a UI component that lets you “accept more than 100 payment methods, validates input, and handles errors,” and its Stripe.js reference says Stripe “inserts an iframe into each div to securely collect payment information.” Hosted Checkout does the same job with a whole page. What comes back to your server is non-sensitive: Stripe’s guide lists the card type, the last four digits and the expiration date as information that “isn’t subject to PCI compliance.”

Where the card is typed decides how much of PCI DSS lands on you. Stripe’s integration security guide says a business that handles card data directly “might be required to meet more than 300 security controls in PCI DSS,” while its low risk integrations send payment information to Stripe without it passing through your servers. Stripe’s security page adds that it checks your integration method and tells you which PCI validation form, the Self-Assessment Questionnaire, to use, with help in the Dashboard for Elements and Checkout users. What the standard itself asks of you is a separate subject: the PCI DSS requirements themselves.

Four things undo the benefit. Building your own card fields that submit to your server brings card numbers back onto it. Logging full request bodies on the payment route can, I’d argue, copy whatever passes through it into your log store. Third-party scripts on the checkout page carry their own risk, which Stripe’s guide states directly: “If they’re ever compromised, an attacker could execute arbitrary code on your page.” And a payment page served over anything older than TLS 1.2 breaks Stripe’s rule that payment pages use TLS 1.2 or above. One more note for older code: Stripe calls the Card Element “a legacy integration” and strongly recommends the Payment Element for card payments too.

The server side: the session, the price and the webhook that grants access

The server owns 3 things in a Stripe integration: the server key, the price, and access. It creates the session from a Price id, never a browser-sent amount, and grants access only from a payment it confirmed, with a signature-verified webhook as the path that always runs. The success page redirect is a thank-you, not proof of payment.

The flow, in six steps:

  1. The browser asks your server to start a purchase and sends only what was chosen, such as a plan key, never an amount.
  2. The server looks up the Price id, creates the Checkout Session (or a PaymentIntent) with its server key, puts your user id in client_reference_id or metadata, and sends an idempotency key, as Stripe’s page on idempotent requests describes, so a retried request cannot create a second session.
  3. The customer pays on Stripe’s surface.
  4. The success page grants nothing by being visited. If it calls your server to speed things up, the server retrieves the Checkout Session and checks its payment_status, as the six-ways article linked above explains.
  5. Stripe sends checkout.session.completed to your webhook endpoint, and for subscriptions also events such as customer.subscription.created and invoice.paid.
  6. The handler verifies the signature against the raw body with that endpoint’s own secret, records the event id so a redelivery does nothing, and only then updates the account.

Stripe’s idempotency layer saves the result of the first request for each key, accepts keys on every POST request, and may prune a key once it is at least 24 hours old. In Node, the second step looks like this (my sketch, built from Stripe’s documented parameters):

const session = await stripe.checkout.sessions.create(
  {
    mode: 'subscription',
    line_items: [{ price: priceIdFor(planKey), quantity: 1 }], // looked up on the server
    client_reference_id: user.id,
    success_url: 'https://example.com/after-checkout?session_id={CHECKOUT_SESSION_ID}',
  },
  { idempotencyKey: purchaseAttemptId },
);

Stripe’s page on fulfilling orders is direct about the order of trust: “You can’t rely on triggering fulfillment only from your checkout landing page, because it’s not guaranteed customers visit that page,” and automatic fulfillment with webhooks “is required if you sell subscriptions or accept payment methods with delayed success notification.” It also recommends calling the same fulfillment function from the landing page, because webhooks can sometimes be delayed. Base44’s payments guide, by contrast, tells builders to confirm the payment and update the account on the success page “instead of relying on webhooks alone.”

Webhook signing secrets are per endpoint and separate from API keys, so the same URL has a different secret in the sandbox and in live mode. When a delivery shows a 200 and the account still did not change, work through the checks for a Stripe webhook that does not update the database in order. In live mode Stripe can retry a failed delivery for up to 3 days with exponential backoff, and it does not guarantee the order events arrive in. So the handler must be safe to run twice and out of sequence; that is my conclusion from Stripe’s two rules.

The topics underneath this flow go deeper than one page can: what is a webhook, why Stripe signature checks fail, how to make a webhook handler idempotent, the subscription lifecycle in Stripe and how to enforce plan limits on the backend. Renewals that bounce are covered in how to handle failed subscription payments. Two neighbors of the integration matter as well: how to set up the Stripe customer portal, for a self-serve billing page, and secrets management best practices, for where the server key and webhook secret should live.

Stripe API versions: pinning one, and what an upgrade changes

A Stripe API version applies in 3 places: the account default, the version your code sends, and each webhook endpoint. Pin it in code and on every endpoint, because an endpoint created without one follows the account default, and a library upgrade can move the version your code sends.

Stripe names each version by its release date plus a release name; the current one, as of 2026-10-03, is 2026-09-30.endive. Since the 2024-09-30.acacia release, Stripe’s API versioning page says, new versions ship monthly with no breaking changes, and twice a year a new major release starts with a version that has breaking changes. Your account’s default gets set the first time you make an API request, and you view and upgrade it in Workbench.

Where the version is setWhat it controlsIf you leave it unset
Account default, in WorkbenchRequests that send no version, and webhook endpoints created without oneSet by your first API request
Your code: the library’s apiVersion option, or the Stripe-Version headerThe shape of the requests and responses your code works withRecent stripe-node uses the version that was latest when that release shipped; stripe-node before v12 uses the account default
Each webhook endpoint (api_version, or snapshot_api_version on an event destination)The shape of the event payloads that endpoint receivesThe account default; it can be set only when the endpoint is created

Why an owner cares: a request made on one version and a webhook rendered on another can disagree about field names, and a default upgrade changes every unpinned endpoint at once (my reading of the table). Stripe’s own advice in its API upgrades guide is to “specify the API version that you’re integrating against in your code instead of relying on your account’s default API version.” Record both values in the repo so the next person can see them.

An upgrade, done the way Stripe’s guide lays it out, has five moves. Read the entries between your version and the target in the Stripe changelog, where versions with breaking changes carry a “Breaking changes” label. Upgrade the library in a branch and run the payment tests in a sandbox. Create a second webhook endpoint on the new version at the same URL with a query parameter, so every event arrives twice and you can compare payloads. Switch processing to the new endpoint, with the old one returning a 400 so Stripe re-sends its events if you need to revert. While both are live, the handler must be idempotent, because Stripe delivers each event to both. Once the new endpoint processes events correctly, disable the old one. The newer /v2 namespace is a separate thing from a dated version: it is a set of endpoints built on different design patterns, while most of the API stays in /v1.

Next.js Stripe integration and Angular notes

A Next.js Stripe integration keeps the server key in Route Handlers and reads the webhook’s raw body before any parsing. An Angular single-page app has no server of its own, so it needs a separate backend or serverless function for the session and the webhook. The publishable key is the only key a browser gets.

The rule that never changes: anything holding the server key runs on a server. In Next.js, Next.js environment variables prefixed NEXT_PUBLIC_ are inlined into the JavaScript sent to the browser, so only the publishable key may carry that prefix. Next.js Route Handlers read the body with standard Web API methods, so the webhook handler calls request.text() and passes that string to Stripe before anything parses it as JSON. Stripe’s signature page says some frameworks edit the body by adding whitespace, reordering keys, converting the string to JSON or changing the encoding, which makes verification fail, and that the error means at least one of the three inputs (the body, the Stripe-Signature header, or the endpoint secret) is wrong.

For Stripe payment gateway integration in Angular, the session and the webhook live in a separate backend, and the browser side loads Stripe.js through the @stripe/stripe-js package rather than a copied script; Stripe’s reference says the script “should always be loaded directly from https://js.stripe.com, rather than included in a bundle or hosted yourself.” A React app without a framework server is the same case as Angular, with Stripe’s React components on top.

FrameworkWhere the server key may liveThe webhook gotchaClient package
Next.js (App Router)Route Handlers and other server code; never a NEXT_PUBLIC_ variableRead the body with request.text() before any parsing@stripe/stripe-js, plus @stripe/react-stripe-js for React components
AngularA separate backend or serverless functionThe backend must hand Stripe the unparsed body; in Express, express.json() goes after the webhook route@stripe/stripe-js
React without a frameworkA separate backend or serverless functionSame as Angular@stripe/stripe-js and @stripe/react-stripe-js
Supabase Edge Function as the backendThe function’s environment (Supabase’s example reads the webhook secret with Deno.env.get)verify_jwt defaults to true; for a Stripe webhook keep verify_jwt = false and verify Stripe’s signature inside the handlerNot applicable

On Supabase, the Edge Functions guide puts it plainly: external providers such as Stripe “sign the request body with their own shared secret,” so the function skips Supabase’s own check and verifies the provider’s signature inside the handler.

How to check your own integration

A Stripe integration is checked with 7 tests: no secret in the client bundle, an edited amount that changes nothing, no card field on your domain, access that arrives with the tab closed, a resent event with one effect, matching pinned versions, and only live ids in production.

Each check runs in a sandbox against your own app, and each procedure comes from Stripe’s documentation or from what the code itself shows; keep a screenshot or a log line as the evidence for each one.

  1. 01 Search the built client bundle for sk_live_, sk_test_, rk_live_, rk_test_ and whsec_ and expect zero hits; a publishable pk_ key is expected there
  2. 02 Open the network tab at checkout and look for an amount in the request your browser sends; change it and confirm the charge does not change
  3. 03 Confirm that no form field on your own domain takes a card number
  4. 04 As a signed-in test user, pay with a test card yourself and close the tab before the redirect; access must still arrive, because the webhook grants it
  5. 05 Take the event that check 4 produced, resend it to your endpoint from the Stripe CLI or the Dashboard, and confirm exactly one effect; then post a captured delivery's body altered by one character, with its original signature header, within five minutes, and confirm a signature-mismatch refusal
  6. 06 Read the API version set in code and the version on the webhook endpoint, and confirm they match
  7. 07 List test and live keys, webhook secrets and price ids side by side and confirm production holds only live ones

For check 5, the CLI command is stripe events resend <event_id> --webhook-endpoint=<endpoint_id>, which works for up to 30 days after the event was created; the Dashboard’s Resend button works for up to 15 days. A stripe trigger fixture sends a signed test event, but it belongs to no user of your app, so in my view it tests signature handling and not the account update. The five-minute window comes from Stripe’s webhooks guide: its libraries “have a default tolerance of 5 minutes between the timestamp and the current time,” so an altered body posted inside that window fails on the signature, not on age. What a mismatch error means, and the three inputs behind it, is on Stripe’s webhook signature page. The reference for resending is stripe events resend.

The full cutover list is in the Stripe test mode to live checklist and the payment go-live checklist. Keep Stripe’s own go-live advice in view too: it recommends logging important data on your end, because your own logs serve as a backup when a problem would stop Stripe from logging the request.

When we run the Production Hardening Sprint, deliverable 5.1, Webhook signature verification, is verified this way: confirm valid events succeed and invalid or altered payloads are rejected. The fifth check above is the same test in its simplest form.

Where the sprint fits

In the Production Hardening Sprint, the payments area covers five parts of this page. Deliverable 5.1 is to verify the payment provider’s signatures before processing webhook payloads ; 5.2 is to make every webhook handler safe to repeat, including concurrent delivery and its downstream side effects ; 5.3 is to handle creation, updates, cancellation, payment failure, refunds, and disputes, including delayed or out-of-order events ; 5.4 is to enforce paid-plan permissions and usage limits in backend actions ; and 5.5 is to verify test and live credentials are isolated by environment. New features that change the product’s core capabilities are separate work; the package includes only the supporting interfaces the listed controls need, such as session management, account deletion and billing self-service. The fee covers our engineering work; hosting, paid tools, and API usage remain in your accounts, and we explain any required third-party costs before enabling them. Every deliverable and its verify step is listed in the published scope.

Common questions about a Stripe integration

Is Stripe integration free?

Mostly, yes: the integration itself has no fee. Stripe’s pricing page, as shown on 2026-10-03, lists “No setup fees, monthly fees, or hidden fees” and charges per successful transaction, 2.9% + 30¢ for domestic cards in its US-dollar pricing. The real cost of integrating is the engineering time to get the seven parts right (my reading).

What is the difference between Stripe Checkout and Stripe Elements?

Checkout is a whole payment page that your server opens with a Checkout Session, hosted by Stripe or embedded on your site; Elements, such as the Payment Element, are form components you place inside a page you build. Stripe’s integration options rate Checkout “Low coding” with “Limited customization” and Elements “More coding” with “Extensive customization with Appearance API”. Both keep card entry on Stripe’s side, and for Elements Stripe recommends the Checkout Sessions API for most integrations.

Can AI agents build Stripe integrations?

Yes, to a working happy path, and Stripe documents how: Stripe’s docs for building with agents offer an agent plugin that bundles its MCP server and agent skills. The seven parts above are what to check afterwards, for the client-trust reason given in the audit numbers earlier on this page.

Stripe’s MCP server gives agents tools to work with the Stripe API and search Stripe’s documentation; for Claude Code, Stripe’s page lists claude mcp add --transport http stripe https://mcp.stripe.com/, and what that connection can reach is covered in the Stripe MCP server for Claude Code.

Do I need an LLC for a Stripe account?

No. Stripe’s support pages say you can sell without a separate business entity by signing up as a sole proprietorship, or as a partnership if you operate with others. For a US account, Stripe’s US account requirements say it verifies your legal entity name, legal entity type (such as Sole Proprietor or LLC), EIN, SSN (or ITIN) and business address. This is not legal or tax advice.

Open the Payment Link in the Dashboard, click Buy button, and paste the embed code Stripe generates (a <script> tag and a <stripe-buy-button> web component) into your page; the buy button uses your account’s publishable key. If the app must unlock anything after the payment, you still need the webhook, because Payment Links use Checkout.