
How to Add Free Trials to a Chrome Extension (2026 Guide, with Code)
How to add free trials to a Chrome extension in 2026: three patterns that actually work, MV3-safe code samples, and the server-side state model that prevents abuse.
How to add free trials to a Chrome extension comes down to picking the right pattern and putting the state of truth on a server you control. There are three patterns that actually work in 2026: card-required Stripe trials, no-card-required time-limited trials, and usage-limited freemium ("3 free uses, then paywall"). The right choice depends on your price point, your audience, and how much abuse you can tolerate. Everything else — local timers, chrome.storage.local flags, install-date heuristics — leaks within a week.
TL;DR
- Pick one of three trial patterns: card-required, no-card time-limited, or usage-limited freemium. Don't mix.
- The trial state must live on a server, keyed to a user identity (magic link, install ID, or both). Local-only state is trivially defeated by uninstall-reinstall.
- With crxpay, the SDK reads a signed entitlement cache, so a "trialing" user gets the same
hasEntitlement('pro')check as a paying user — no extra code path.
Why free trials in Chrome extensions are tricky
Chrome extensions on Manifest V3 are not normal web apps. The service worker dies after 30 seconds of inactivity and your in-memory state dies with it. Anything stored in chrome.storage.local lives on the user's disk and is wiped the moment they uninstall the extension. The system clock is a user-controlled input — set it forward two weeks and a naive "trial expires in 14 days" check is already over.
That leaves three sources of truth you can actually trust:
- A server you control.
- A signed payload from that server, cached locally with an expiry the SDK validates cryptographically.
- The Stripe subscription object, which has its own
trialingstatus andtrial_endtimestamp.
Anything else is a guess. The whole reason RevenueCat exists for mobile, and the reason crxpay exists for Chrome extensions, is to put a thin trusted layer between your extension and these realities so you stop writing the same buggy trial-clock code in every extension.
The three trial patterns that work
Card-required Stripe trial. The user enters a card up front, Stripe holds a $0 authorization, the subscription starts in trialing status, and on day N it auto-converts to paid unless cancelled. Highest trial-to-paid conversion. Lowest trial signup rate. Best for products with a clear "I will use this" intent — productivity, work tools, B2B.
No-card time-limited trial. The user identifies themselves (magic link email or a generated install ID) and gets N days of full access without entering a card. Highest signup rate, lowest conversion. Best for consumer-facing extensions where the install-to-trial friction matters more than the trial-to-paid drop-off.
Usage-limited freemium. No clock at all. The user gets X free actions — 3 summarizations, 5 translations, 10 saved clips — then a paywall. Best for extensions where the value is immediate and measurable per-action. This is more of a freemium model than a trial, but users perceive it as a trial, so it belongs on the list. See monetization models for when freemium beats a fixed trial window.
Pick one. Mixing patterns confuses users and makes your conversion data unreadable.
How to add a free trial to a Chrome extension — step by step
The rest of this guide walks through the card-required and no-card patterns using the crxpay SDK. The usage-limited pattern is a different shape (entitlements with consumption metering) and is covered in /features/paywalls.
1. Define the trial product in the dashboard
In dashboard.crxpay.io, create a Product (e.g. "Pro"), attach a Price (e.g. $5/month), and on the Price configure trial_period_days: 7. That writes through to your connected Stripe account as a Price object with a trial period attached. Give your Pro entitlement a stable key like pro — that's the string your client code will check. The dashboard walkthrough is at /features/trials.
2. Initialize the SDK in your service worker
// background.ts
import { CrxPay } from '@crxpay/sdk';
const cp = new CrxPay({
publishableKey: 'pk_live_xxx',
// SDK keeps a signed entitlement cache so this works offline
// and survives the MV3 30-second service worker death.
cache: 'signed',
});
// Identify the user as early as possible. Magic-link auth gives you
// a stable userId; for no-auth flows, cp.installId() returns a
// per-install UUID generated and persisted by the SDK.
chrome.runtime.onInstalled.addListener(async () => {
const userId = await getOrCreateUserId(); // your auth flow
await cp.identify({ userId });
});
The cache is HMAC-signed by the server and validated by the SDK on every read, so a user editing chrome.storage.local to flip status: 'active' gets rejected on the next check. This is the "offline-first" part of the SDK that ExtPay famously doesn't do — see /migrate-from-extpay for the comparison.
3. Gate features on entitlement, not on status
The single most common mistake in trial code is branching on status === 'active'. That excludes trialing users, who should have full Pro access. Branch on the entitlement instead.
// content-script.ts (or popup, options, anywhere)
import { CrxPay } from '@crxpay/sdk';
const cp = new CrxPay({ publishableKey: 'pk_live_xxx' });
const sub = await cp.subscription.get();
if (sub.hasEntitlement('pro')) {
// Works for paying users AND trialing users.
// The dashboard grants the 'pro' entitlement automatically
// for the full trial period.
unlockProFeatures();
} else {
showPaywall();
}
// Show a friendly trial countdown without branching on status:
if (sub.status === 'trialing' && sub.trialEndsAt) {
const days = Math.ceil((sub.trialEndsAt - Date.now()) / 86_400_000);
renderBanner(`Trial ends in ${days} day${days === 1 ? '' : 's'}`);
}
hasEntitlement('pro') is a single signed-cache lookup. No network call on the hot path. The SDK refreshes the cache on a sane schedule and on auth events.
4. Start the trial from a checkout button
For card-required trials, send the user to Stripe Checkout with trial_period_days set. crxpay handles the redirect and the return URL.
// popup.ts
import { CrxPay } from '@crxpay/sdk';
const cp = new CrxPay({ publishableKey: 'pk_live_xxx' });
document.getElementById('start-trial')!.addEventListener('click', async () => {
await cp.checkout.start({
priceId: 'price_1Q...',
trialDays: 7,
successUrl: 'https://yourext.com/welcome',
cancelUrl: 'https://yourext.com/pricing',
});
});
For no-card trials, skip Checkout entirely. Call cp.trials.start() and the server creates a trialing subscription with no payment method attached. When the trial ends, the subscription transitions to incomplete and the entitlement is revoked.
await cp.trials.start({
entitlement: 'pro',
durationDays: 14,
});
Both flows end up at the same place: a server-side subscription record with status: 'trialing', a trial_end timestamp, and the pro entitlement granted until that timestamp.
5. Handle trial-end conversion and cancellation
You don't have to write any code for the happy path. Stripe webhooks fire on trial end, the crxpay backend updates the subscription row, the entitlement either stays granted (paid) or gets revoked (cancelled), and the next time the extension reads the cache it sees the new state.
What you do want to write is the UX for trial-end. The SDK fires events you can subscribe to:
cp.on('subscription.updated', (sub) => {
if (sub.status === 'active' && sub.previousStatus === 'trialing') {
showToast('Welcome to Pro');
}
if (sub.status === 'canceled' && sub.previousStatus === 'trialing') {
showWinbackPaywall(); // see /features/paywalls
}
});
Why local timer trials fail
Every "free trial Chrome extension code" tutorial older than 2024 tells you to do this:
// DON'T DO THIS
const installedAt = await chrome.storage.local.get('installedAt');
if (Date.now() - installedAt > 7 * 86_400_000) {
showPaywall();
}
This is broken in four independent ways:
- Uninstall-reinstall resets the trial.
chrome.storage.localis wiped on uninstall. A user can have an infinite trial by uninstalling every 7 days. Five seconds of work. chrome.storage.syncdoesn't save you. It syncs across the same Google account, so a user with two Google profiles gets two trials. Also wiped on uninstall.- The system clock is user-controlled. Set the clock back, the trial never ends. Set it forward during install, the trial never starts.
- Service worker amnesia. Any in-memory state you used to debounce checks is gone after 30 seconds idle. You'll re-read storage on every popup open, which is fine, but you can't keep a single in-memory "expires at" timer running.
The fix is the obvious one and the only one: the trial expiry lives on a server, the server checks it, and the SDK trusts a signed response. That's what /features/trials does for you.
Card-required vs no-card: which converts better?
Public benchmarks from Paddle and RevenueCat consistently show the same shape across consumer subscription apps:
- Card-required trials convert to paid at roughly 40–60% of trial starts.
- No-card trials convert to paid at roughly 5–15% of trial starts.
- But no-card trials get 3–5x more trial starts, because the friction to sign up is lower.
The math usually favors card-required for higher-priced products (anything above $5/month) and no-card for low-priced products or those where you need user volume first to prove value. For extensions specifically, where users are already comfortable with "install and use immediately," no-card with a clear paywall on day 8 tends to win on total revenue — but it depends on the price point. The extensions in /blog/chrome-extensions-that-make-money-2026 skew card-required because they're priced like SaaS, not like games.
If you're unsure, ship one, measure for 30 days, then try the other. Both are a one-line config change in the dashboard at /features/trials.
Trial-end notifications: don't be shady
A trial that ends silently and bills the card is the fastest way to a chargeback and a 1-star review. The honest cadence is:
- Day T-3: "Your trial ends in 3 days. You'll be charged $5 on the 8th. Cancel anytime."
- Day T-1: "Your trial ends tomorrow."
- Day T (after charge): "Thanks. Here's your receipt."
Because crxpay uses magic-link auth for the dashboard side, you already have an email for every trialing user — no separate email collection step. The dashboard ships these three emails by default for card-required trials. For no-card trials it ships a single day-of "your access has ended, here's the upgrade link" email.
What if a user cancels mid-trial?
Keep them on Pro until the trial-end date. Then revoke.
This is the Stripe default behavior with cancel_at_period_end: true, and it's what crxpay does. The user gets what they signed up for — N days of access — and you don't get a refund request. The subscription transitions trialing → canceled at trial_end, the webhook fires, the entitlement is revoked, the SDK cache invalidates on next read, the paywall comes up.
The one place to be careful is if your trial includes server-side compute you'd like to stop paying for the moment the user cancels (e.g. an AI quota). Read the cancel_at field on the subscription — that's the future revocation timestamp. You can soft-warn the user that cancelling won't end access early, so they don't expect a partial refund.
Wrap
Free trials in Chrome extensions are a solved problem the moment you put the state on a server and read it through a signed cache. Pick card-required or no-card, define the price in the dashboard, gate UI on hasEntitlement('pro'), and let the webhook handle the rest. The whole flow above is the default crxpay setup — there's no extra code, just configuration.
If you want to ship a trial this weekend: crxpay is free until your extension hits $2,500 in revenue, then 2.5% per transaction — half of what ExtPay charges. The SDK is @crxpay/sdk, the dashboard is dashboard.crxpay.io, and the React paywall components in @crxpay/react-paywall cover the upgrade UI if you don't want to design your own. Start at /features/trials and you'll have a working trial in under an hour.
FAQ
Q: Can I add a free trial to a Chrome extension without using Stripe? A: Yes — the no-card pattern doesn't require a payment processor at trial-start, only at conversion. crxpay supports starting a no-card trial against any of its connected payment backends (Stripe Connect today, Paddle and LemonSqueezy on the roadmap), and the user only sees a checkout when the trial ends.
Q: Will users cheat the trial by uninstalling and reinstalling? A: Not if the trial state is server-side and keyed to a user identity. Magic-link auth ties the trial to an email; no-auth flows use a per-install UUID that survives reinstall only if the user signs in. A determined user with multiple emails can always get multiple trials — accept that and design the trial length around it.
Q: How do I show "trial ends in 4 days" without making a network call every time the popup opens?
A: Read subscription.trialEndsAt from the signed local cache. The crxpay SDK keeps the cache fresh in the background and validates the HMAC signature on every read, so the popup gets an answer in under a millisecond without hitting the network.
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "Can I add a free trial to a Chrome extension without using Stripe?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Yes — the no-card pattern doesn't require a payment processor at trial-start, only at conversion. crxpay supports starting a no-card trial against any of its connected payment backends, and the user only sees a checkout when the trial ends."
}
},
{
"@type": "Question",
"name": "Will users cheat the trial by uninstalling and reinstalling?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Not if the trial state is server-side and keyed to a user identity. Magic-link auth ties the trial to an email; no-auth flows use a per-install UUID that survives reinstall only if the user signs in."
}
},
{
"@type": "Question",
"name": "How do I show trial countdowns without a network call on every popup open?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Read subscription.trialEndsAt from the signed local cache. The crxpay SDK keeps the cache fresh in the background and validates the HMAC signature on every read, so the popup gets an answer in under a millisecond without hitting the network."
}
}
]
}