stripe-js: a loader for Stripe.js, and a lesson in what not to bundle
Loading wrapper for Stripe.js
At a glance
- What is it?
- The @stripe/stripe-js package is small on purpose. It does not contain Stripe.js, it cannot, because PCI compliance requires that script to be loaded directly from js.stripe.com, and all the package does is insert that script tag and hand you a typed global. The interesting decisions are the side effect, the pure variant, and the fact that the npm version tells you nothing about the Stripe.js version you actually get.
- Who is it for?
- Adopt @stripe/stripe-js if you are putting a Stripe integration in a JavaScript application and want typed imports and a managed script tag, since it handles the loading and the type declarations correctly. Do not try to bundle Stripe.js itself or self-host it, because the README states plainly that PCI compliance requires loading it directly from js.stripe.com.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 25, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The package is a loader, not the library, and that is a compliance decision
The @stripe/stripe-js package allows Stripe.js to be imported as a CommonJS module or ES module. That is the whole scope, and the reason it is so narrow is written at the top of the README.
To be PCI compliant, you must load Stripe.js directly from https://js.stripe.com. You cannot include it in a bundle or host it yourself. This package wraps the global Stripe function provided by the Stripe.js script as an ES module.
So the file you install from npm contains no payment code at all. It contains the logic that inserts a script tag pointing at js.stripe.com, and a promise that resolves once the global is there. If you have ever wondered why the Stripe integration in a bundled React app is mysteriously the one thing that is not in the bundle, this is the answer, and it is not a packaging oversight.
The consequence for the package is that it has almost nothing to do at runtime, and everything to do with correctness around loading, typing and version reporting.
On the versioning, there is a distinction worth internalising. Calling loadStripe always loads the latest version of Stripe.js, regardless of which version of @stripe/stripe-js you use. Updates for this package only impact the tooling around the loadStripe helper itself and the TypeScript type definitions provided for Stripe.js, and they do not affect runtime availability of features of Stripe.js.
That is unusual and it is deliberate. The npm version tracks the tooling, not the payment runtime you actually get.
The version table is a codename map, not a Stripe.js version map
The versioning table in the README looks like a small oddity and is actually the clearest statement of how this package relates to Stripe.js.
Each @stripe/stripe-js version is pinned to a specific Stripe.js version, and the pinned versions are as follows. Versions below 6 map to Stripe.js v3. Version 6 maps to acacia, v7 to basil, v8 to clover, v9 to dahlia, and v10 to endive.
Those codenames are how Stripe.js releases are actually named, and the mapping is one-directional in a way that catches people out. The package version tells you which Stripe.js release line the type definitions and the tooling were written against, and the README notes that the type declarations track a specific Stripe.js version, with a link to the Stripe.js versioning documentation.
But the runtime is not pinned by your package version. loadStripe always loads the latest version of Stripe.js. So if you install @stripe/stripe-js v10 and your page loads Stripe.js endive or something newer, you are running a newer runtime than the types describe.
The two things that make this tolerable are stated separately in the README. Some methods in Stripe.js accept and return objects from the Stripe API, and the type declarations in @stripe/stripe-js for these objects will always track the latest version of the Stripe API. And the package may release new minor and patch versions with small but backwards-incompatible fixes to the type declarations, changes that do not affect Stripe.js itself.
That last point is the one that matters in practice. A patch bump of this package can break your typecheck. If you pin the package version, you pin the types, and pinning the types means you can be out of step with the runtime.
loadStripe, and the promise that resolves to null on a server
The main entry point is one function, and the shape of it is what you would expect from a loader that has to work in every environment.
This function returns a Promise that resolves with a newly created Stripe object once Stripe.js has loaded. It takes the same parameters passed when directly initializing a Stripe instance, and if necessary it will load Stripe.js for you by inserting the Stripe.js script tag.
The import and the call look like this:
import {loadStripe} from '@stripe/stripe-js';
const stripe = await loadStripe('pk_test_TYooMQauvdEDq54NiTphI7jx');The README notes that a random API key has been placed in this example and that you should replace it with your actual publishable API keys to test the code through your own Stripe account. It is a publishable key, so it is not a secret, but it is still not yours.
There is one behaviour that catches people during server-side rendering. If you call loadStripe in a server environment it will resolve to null.
That is not a bug to work around, it is the design. On a server there is no document, so there is no script tag to insert and no global to wrap, and the function reports that by resolving to null rather than throwing. The consequence for your code is that you need to handle the null case, and if your framework renders on the server and hydrates on the client, the client will get a real object where the server got null.
If you have deployed a Content Security Policy, the README tells you to make sure to include Stripe.js in your directives, with a link to the Stripe security guide. That is a direct consequence of the script coming from a third-party origin, and it is the other thing that tends to break during deployment rather than during development.
The side effect, and why the script should be on every page
The most consequential behaviour of this package is invisible: by default, this module will insert a script tag that loads Stripe.js from https://js.stripe.com, and this happens as a side effect immediately upon importing this module.
An import with a side effect is normally a smell, and here it is deliberate. The reason is fraud detection, and the README states it directly. To best leverage Stripe's advanced fraud functionality, ensure that Stripe.js is loaded on every page, not just your checkout page, and this allows Stripe to detect suspicious behavior that may be indicative of fraud as customers browse your website.
The failure mode if you get it wrong is subtle. If you utilize code splitting or only include your JavaScript app on your checkout page, the Stripe.js script will only be available in parts of your site. The advanced fraud signals are collected across the whole session, so a customer whose first touch was a product page is missing the context that the signals depend on.
So there are two documented ways to load it everywhere. The first is to import the package as a side effect in code that will be included throughout your site, for example your root module:
import '@stripe/stripe-js';The second is to manually add the script tag to the head of each page, and if an existing script tag is already present, this module will not insert a new one, and when you call loadStripe it will use the existing script tag:
<!-- Somewhere in your site's <head> -->
<script src="https://js.stripe.com/dahlia/stripe.js" async></script>The deduplication behaviour in that second path is what makes it safe to combine with the first: you cannot end up with two script tags if the module checks for an existing one.
The pure entry point, and setLoadParameters for turning fraud signals off
The alternative entry point solves the opposite problem, and it is the one to reach for when a side effect on import is unacceptable.
If you would like to use loadStripe in your application but defer loading the Stripe.js script until loadStripe is first called, use the alternative @stripe/stripe-js/pure import module:
// CommonJS module import
const {loadStripe} = require('@stripe/stripe-js/pure');
// ES module import
import {loadStripe} from '@stripe/stripe-js/pure';
// Stripe.js will not be loaded until `loadStripe` is called
const stripe = await loadStripe('pk_test_TYooMQauvdEDq54NiTphI7jx');The same path carries the one configuration knob. If you would like to disable advanced fraud detection altogether, use loadStripe.setLoadParameters:
loadStripe.setLoadParameters({advancedFraudSignals: false});
const stripe = await loadStripe('pk_test_TYooMQauvdEDq54NiTphI7jx');And the constraint is explicit: the loadStripe.setLoadParameters function is only available when importing loadStripe from @stripe/stripe-js/pure.
That is a good design because the two capabilities are genuinely coupled. You cannot sensibly configure load parameters on a script that has already been inserted as an import side effect, since the parameters have to be set before the load happens. So the configuration lives only on the entry point where the load is under your control.
The trade-off is that giving up the side effect means giving up the automatic everywhere-loading, and with it the fraud signal collection that depends on it. If you take the pure route, you are making a deliberate decision about which page loads the script.
Types, tooling and a repository that does not accept pull requests
The typing story is thorough, and it has one sharp edge.
The package includes TypeScript declarations for Stripe.js, and the support statement is for projects using TypeScript versions >= 3.1, with the minimum requirements listed as Node.js v12.16 and TypeScript v.3.1.1. The engine field in package.json says node >=12.16.
The sharp edge is that the declarations for the API object types track the latest version of the Stripe API, always. If you are on an older Stripe API version, the recommendation is to update to the latest version, or to ignore and override the type definitions as necessary. So the type layer is deliberately kept current rather than pinned to your account's API version, and the cost of that choice is that it can describe fields your account does not have yet.
The build and test setup is the part of this repository that shows the most care. The test script chains five steps: lint, unit tests with jest, a package types check with attw, a types test driven by zx, and a typecheck with tsc. The attw step, arethetypeswrong, is the tool that checks whether a published package's type entry points are actually resolvable, and running it against both the lib and pure entry points is a check many packages skip.
There is a type snapshot mechanism, update-ts-snapshot, which suggests the public type surface is treated as something to review deliberately rather than as an accident. The build uses rollup, there are examples/ directories for parcel and rollup, and the publishing is done through a script, with a prepublishOnly hook that fails with a message telling you to use the publish script instead.
One thing to know before you plan a contribution: this project is maintained by Stripe and does not accept external pull requests. Feedback and ideas go through an issue form.
Editorial conclusion
Adopt @stripe/stripe-js if you are putting a Stripe integration in a JavaScript application and want typed imports and a managed script tag, since it handles the loading and the type declarations correctly. Do not try to bundle Stripe.js itself or self-host it, because the README states plainly that PCI compliance requires loading it directly from js.stripe.com. Before you deploy, check which pages need the script, because by default importing the module inserts the script tag as a side effect on that page only, and use the pure entry point if you need to defer the load until you call loadStripe.
Frequently asked questions
Why can I not bundle Stripe.js with my application?
To be PCI compliant you must load Stripe.js directly from https://js.stripe.com. You cannot include it in a bundle or host it yourself, which is why the npm package only inserts a script tag and wraps the global Stripe function rather than shipping the library.
What is the difference between @stripe/stripe-js and @stripe/stripe-js/pure?
The main module inserts the Stripe.js script tag as a side effect immediately on import. The pure module does not load Stripe.js until loadStripe is first called, and it is the only entry point where loadStripe.setLoadParameters is available.
Does the @stripe/stripe-js version determine which Stripe.js I get?
No. Calling loadStripe always loads the latest version of Stripe.js, regardless of which version of the package you use. The package version is pinned to a Stripe.js version for the type definitions, but the runtime is not pinned by it.
What happens if I call loadStripe on the server?
It resolves to null, because there is no document to insert a script tag into and no global to wrap. Your code needs to handle that null case, which matters most in frameworks that render on the server and hydrate in the browser.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/stripe-stripe-js)Was this useful?