Library / SDK
Glench/ExtPay avatar
Glench/ExtPay

ExtPay: taking payments inside a browser extension without running a server

The JavaScript library for ExtensionPay.com — payments for your browser extensions, no server needed.

784 stars89 forksJavaScriptNOASSERTION

At a glance

What is it?
ExtPay is a JavaScript library that gives a browser extension a paid state, a checkout page, a trial, a login and a subscription management flow, with the extension creator never operating a payment backend. The library is injected into the background script, needs a storage permission in the manifest, and talks to a hosted payment service from the extension itself.
Who is it for?
ExtPay fits a solo extension developer who wants a paid tier or a subscription without integrating Stripe, handling webhooks and building a login system, and who is comfortable sending their users to an external checkout page. It does not fit a team that needs the payment flow to live on its own domain, or a project with a strict policy against third-party calls from the extension.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 19 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The whole model in one snippet

ExtPay is the JavaScript library behind ExtensionPay.com, a service for adding payments to browser extensions, and the README puts the entire flow in a few lines:

js
// Example code
// your-extension/background.js
const extpay = ExtPay('your-extension-id');
extpay.startBackground();

extpay.getUser().then(user => {
    if (user.paid) {
        // ...
    } else {
        extpay.openPaymentPage()
    }
})

That is the product in miniature. You initialise with your extension id, you ask whether the current user is paid, and if not you send them to a payment page. There is no server for you to write, no webhook endpoint to expose and no database of subscribers for you to maintain, because the hosted service holds that state and your extension asks it about the current user.

The README quotes a user describing the same experience, saying that the setup took less than an hour and that they would not have monetised at all if they had to use Stripe directly, and that the service has earned extension creators over 800k dollars and counting.

The library uses Mozilla's webextension-polyfill internally for compatibility across browsers, which means it should work on almost all modern browsers, and the README points at a sample extension for both manifest versions as the way to learn by example.

The manifest requirements are the real integration cost

Two things have to be in the extension manifest, and both of them are visible to the user or to the browser, which is why they are worth reading carefully.

First, the storage permission:

json
{
    "permissions": [
      "storage"
    ]
}

The note that follows is the important one. ExtPay will not show a scary permission warning when users try to install your extension, which is a claim about how the permission is used rather than about the permission list itself.

For Firefox, the manifest may also need "https://extensionpay.com/*" in the extension manifest's permission for ExtPay to work properly.

And if you have a content_security_policy in your manifest, or you get a Refused to connect error, you have to add connect-src https://extensionpay.com to the extension's content security policy, with a link to the Mozilla documentation on the subject.

The install itself is a single command:

bash
npm install extpay --save

If you are not using a bundler, you can copy the dist/ExtPay.js file into your project, or the ExtPay.module.js for ESM and the ExtPay.common.js for CommonJS, and the package manifest points at those same three files for the main, module, unpkg and jsdelivr entries.

The service worker catch that costs everyone an hour

The library has to be present in the background file, which is often named something like background.js, and the README is emphatic that it will not work correctly without it. With a bundler you can import or require it right in the background file.

The manifest V3 arrangement is a service worker:

json
{
    "background": {
        "service_worker": "background.js"
    }
}

And the background script does this:

js
// background.js

importScripts('ExtPay.js') // or `import` / `require` if using a bundler

var extpay = ExtPay('sample-extension'); // Careful! See note below
extpay.startBackground(); 

The note about service workers is the trap in this library, and it is worth reading twice. In the example above, extpay will become undefined when accessed in service worker callbacks, so to use extpay in a callback you have to redeclare it:

js
chrome.storage.local.get('foo', function() {
    var extpay = ExtPay('sample-extension');
    // ...
})

And the instruction that follows is the one people get wrong: make sure not to use extpay.startBackground() in callbacks, because it should only be called once.

For manifest V2 without a bundler, the file goes into the manifest itself, with the background scripts array listing both ExtPay.js and background.js, and the background script uses the same two calls, const extpay and startBackground.

This is a real sharp edge in an otherwise simple integration, and it is the kind of thing that produces an extension that works in development and breaks in production.

What the user object tells you

The getUser call makes a network request to the service and returns a user object, and the properties on it are the vocabulary for gating features.

There is a paid flag, which is true or false, and is meant to be a simple way to tell whether the user should have paid features activated. The subtlety is that for subscription payments, paid is only true if subscriptionStatus is active, so a lapsed subscriber is not a paid user in the code, which is the behaviour you want but not the behaviour most people assume.

There is paidAt, a Date object for when the user first paid, or null. There is email, the user's email if there is one or null. There is installedAt, a Date object for when the user installed the extension, which is what a trial is measured from. There is trialStartedAt, null or a Date object for when the user confirmed the free trial. And there is plan, which is null or the plan the user is on, described as an object with fields like unitAmountCents, currency, nickname, intervalCount and interval.

The getUser call can throw on network failure, and the documentation asks you to consider that possibility in your code, showing the promise form with a catch handler. That is worth taking seriously, because a paid feature that fails open on a network error is a feature you have given away.

The rest of the API follows the same pattern: openPaymentPage to let the user pay, openPaymentPage again to manage a subscription, openTrialPage for a free trial, openLoginPage for a user who has already paid, getPlans to list available plans, and onPaid.addListener to run code when payment completes.

Editorial conclusion

ExtPay fits a solo extension developer who wants a paid tier or a subscription without integrating Stripe, handling webhooks and building a login system, and who is comfortable sending their users to an external checkout page. It does not fit a team that needs the payment flow to live on its own domain, or a project with a strict policy against third-party calls from the extension. Before adopting it, read the manifest requirements, and check the pricing and the plan model, since the paid flag is tied to an active subscription rather than to a historical payment.

Frequently asked questions

What does ExtPay do for a browser extension?

It is the JavaScript library behind ExtensionPay.com, giving an extension a paid state, a checkout page, a free trial, a login for existing subscribers and subscription management, without the creator running a payment server. The library initialises with your extension id and calls the hosted service to find out whether the current user is paid.

How do I install ExtPay and what does the manifest need?

With `npm install extpay --save`, or by copying dist/ExtPay.js, ExtPay.module.js or ExtPay.common.js into the project. The manifest needs the storage permission for both manifest v2 and v3, Firefox may also need the extensionpay.com origin in the permissions, and a content security policy has to include connect-src https://extensionpay.com.

What is the service worker gotcha with ExtPay?

In a manifest V3 service worker, the extpay variable becomes undefined when accessed in service worker callbacks, so it has to be redeclared inside the callback. The startBackground call must not be made in a callback either, because it should only be called once.

What does the user object returned by extpay.getUser() contain?

A paid flag which is only true for an active subscription, a paidAt date, an email, an installedAt date, a trialStartedAt date and a plan object with fields such as unitAmountCents, currency and interval. The call makes a network request and can throw on failure, so the documentation asks you to handle that case.

How current is ExtPay?

The package version is 3.1.2, with a release that fixed a manifest v2 sample extension bug in October 2025, and an earlier release that added multiple plans, coupon codes and Stripe Checkout. The last push to the repository was on 2026-10-06, and the repository uses a custom licence (the package.json declares AGPL-3.0-or-later).

Official sources

  1. Glench/ExtPay on GitHub
  2. Issues
  3. Project website
  4. README
  5. Releases
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.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/glench-extpay.svg)](https://hysenlabs.com/projects/glench-extpay)

Was this useful?