crxpaydocs

Quickstart

Take your first crxpay payment in 5 minutes. Real keystrokes, no hand-waving.

This guide gets you from "empty extension folder" to "a real test payment lands in my Stripe account" in under five minutes.

1. Sign up & register your extension

  1. Create a crxpay account

    Sign up at crxpay-dashboard.vercel.app/signup using your email. We'll email you a magic link — no password, no credit card.

  2. Add your extension

    On the dashboard home, click Add extension. You only need to enter:

    • Name — what your users see (e.g. PixelPerfect)
    • Chrome Extension ID — leave blank until you upload to the Web Store; you can add it later

    That's it. We immediately mint your public API key (crxpay_pub_…) and create a default product/price/offering so the SDK has something to render.

  3. Connect Stripe

    Click Settings → Stripe and hit Connect with Stripe. We use Stripe Express Connect, which means:

    • One click. No paste-the-key form.
    • You sign in with your existing Stripe account (or create one — Stripe pre-fills everything from your crxpay profile).
    • You get a Stripe Express Dashboard for payouts; we get permission to create products/prices/checkouts on your behalf.
    • We can never charge you, refund you, or move your money. We can only initiate charges that go directly into your account.

    After connecting, the dashboard topbar shows a Test mode toggle. Leave it on Test for the rest of this guide.

2. Install the SDK

npm install @crxpay/sdk
# or
pnpm add @crxpay/sdk

The package weighs ~14KB gzipped and has zero runtime dependencies. It exports three entry points — one per Chrome extension context.

Entry pointUse inWhat it does
@crxpay/sdk/backgroundService workerOwns the network, signed cache, and state machine. Configure once here.
@crxpay/sdkPopup, options, sidepanelProxy to background via chrome.runtime.sendMessage.
@crxpay/sdk/contentContent scriptsSame as above but routed through the page bridge to dodge MV3 CORS.

3. Wire up background.js

This is the only file where you call configure(). The background script owns the SDK's state and acts as the postman for popup/content calls.

import { CrxPay } from '@crxpay/sdk/background';

const client = CrxPay.configure({
  apiKey: 'crxpay_pub_YOUR_KEY_HERE',  // paste from dashboard → API keys
  refreshInterval: 'hourly',           // chrome.alarms-based, MV3-safe
  debug: true,                         // logs cache hits/misses (dev only)
});

// Required: route SDK messages from popup/content to the background instance.
// The SDK uses CRXPAY_* message types so it never collides with yours.
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
  if (message.type?.startsWith('CRXPAY_')) {
    client.handleMessage(message, sender).then(sendResponse);
    return true; // keep the channel open for async response
  }
});

// Optional: react the moment a user pays — no polling, no page reload.
client.onPaid.addListener((subscription) => {
  console.log('🎉 paid!', subscription.status);
});

4. Wire up popup.js

import { CrxPay } from '@crxpay/sdk';

const result = await CrxPay.getSubscription();

if (result.ok && result.data.hasEntitlement('pro')) {
  document.getElementById('pro-features').style.display = 'block';
} else {
  document.getElementById('upgrade-btn').addEventListener('click', () => {
    CrxPay.openCheckout();   // opens hosted /pay page in a new tab
  });
}

5. Wire up content.js (optional)

If your extension reads subscription state from the page (e.g. to gate features in the user's Twitter feed), use the content-script entry. The API is identical to popup.js.

import { CrxPay } from '@crxpay/sdk/content';

const result = await CrxPay.getSubscription();
if (result.ok && result.data.hasEntitlement('pro')) {
  injectProBanner();
}

6. Update manifest.json

{
  "manifest_version": 3,
  "permissions": [
    "storage",
    "alarms"
  ],
  "host_permissions": [
    "https://api.crxpay.io/*"
  ],
  "background": {
    "service_worker": "background.js",
    "type": "module"
  },
  "action": { "default_popup": "popup.html" }
}
FieldWhy
storageWe persist the signed cache to chrome.storage.local
alarmsThe MV3-safe replacement for setInterval — used for hourly refresh
host_permissions: api.crxpay.ioLets the background script fetch() our API
"type": "module"Required because @crxpay/sdk ships ES modules

Full reference: Manifest V3 setup.

7. Identify the user (when you have their email)

await CrxPay.identify('jane@example.com');

You can call this whenever you have an email — after Google OAuth, after a manual sign-in form, after a Chrome identity.getProfileUserInfo() call. The first call creates a Customer row in our DB and links it to a per-install anonymous ID we generated on first run.

8. Take a test payment

  1. Reload your extension (chrome://extensions → reload icon).
  2. Open the popup → click your Upgrade button.
  3. Stripe's hosted checkout opens in a new tab, prefilled with your test product + price.
  4. Use Stripe's test card: 4242 4242 4242 4242 · any future expiry · any CVC · any ZIP.
  5. Click Pay → you'll land on the success page.
  6. Switch back to the popup, reopen it. The Pro features are unlocked.

In the dashboard, switch to Customers — you'll see the test customer that just paid, with their email, subscription status, and the pro entitlement granted.

9. Listen for the payment in real time

In step 3 you registered an onPaid listener in background.js. As soon as Stripe fires customer.subscription.created to our webhook, the listener fires in your service worker — while the user is still on Stripe's success page. No polling.

import { CrxPay } from '@crxpay/sdk';

CrxPay.onPaid.addListener((subscription) => {
  showThankYouToast();
  if (subscription.hasEntitlement('pro')) renderProBadge();
});

The same event is broadcast to all open popup/content listeners via chrome.runtime.onMessage. You don't need to manage that — just call addListener from any context.

What just happened?

1. User clicked "Upgrade"           SDK opened our hosted /pay page in a new tab
2. User picked a plan + entered card Stripe Checkout (hosted) processed the payment
3. Stripe fired a webhook            Our API received `customer.subscription.created`
4. Our API updated D1                Subscription + entitlement rows written
5. Our API broadcasted to your SDK   Background script received `paid` push
6. SDK refreshed the signed cache    `chrome.storage.local` now has the new state
7. SDK fired onPaid                  Your listener runs in popup + background

The only code you wrote was configure(), the message router, and the entitlement check. Everything else is library code.

Where to go next

Was this page helpful?

Your feedback shapes what we document next.