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
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.
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.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 point | Use in | What it does |
|---|---|---|
@crxpay/sdk/background | Service worker | Owns the network, signed cache, and state machine. Configure once here. |
@crxpay/sdk | Popup, options, sidepanel | Proxy to background via chrome.runtime.sendMessage. |
@crxpay/sdk/content | Content scripts | Same 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" }
}
| Field | Why |
|---|---|
storage | We persist the signed cache to chrome.storage.local |
alarms | The MV3-safe replacement for setInterval — used for hourly refresh |
host_permissions: api.crxpay.io | Lets 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
- Reload your extension (
chrome://extensions→ reload icon). - Open the popup → click your Upgrade button.
- Stripe's hosted checkout opens in a new tab, prefilled with your test product + price.
- Use Stripe's test card:
4242 4242 4242 4242· any future expiry · any CVC · any ZIP. - Click Pay → you'll land on the success page.
- 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
- Stripe Connect setup — go beyond the default product, create your own pricing
- Manifest V3 setup — bundler config, CSP, host permissions
- Manifest V3 guide — the six MV3 footguns and how the SDK avoids them
- SDK reference — every method documented
Was this page helpful?
Your feedback shapes what we document next.