Toolaby

Chrome extension subscriptions with Stripe

Your Chrome extension sells a monthly or yearly plan on your own Stripe account, with a free trial, renewals, cancellation and webhooks.

By the end of this guide, your Chrome extension sells a monthly or yearly subscription, billed by Stripe on your own Stripe account. Toolaby follows each subscription from Stripe's events and unlocks the extension while its status is active or trialing. You will know what each renewal, failed payment and cancellation does.

Before you begin

  • A tool with your extension wired to it, on Test. See the Quickstart.
  • Your paid action calls toolaby.gate() and answers a refusal with toolaby.showPaywall(). See Gate your extension.
  • Nothing from Stripe on Test: Toolaby makes a Stripe test account for your workspace.

1. Add a monthly or yearly plan

On the tool's Pricing page, select New plan. Set Billing to subscription and Interval to monthly or yearly, then enter the amount and currency. Each plan is a Price under the tool's Product on your Stripe account.

Or from the terminal:

npx -y toolaby@latest plan add <tool-id> --billing subscription --interval month --amount 4 --currency usd
FieldOption
Interval--interval month or yearHow often Stripe bills the buyer's card.
Name--name ProOptional. Pro shows as Pro · Monthly.
Free trial--trial-days 70 to 365 days. Checkout takes the card and charges nothing until the trial ends. One trial per inbox per tool.
Devices--devices 21 to 10 devices signed in at once. Left empty, 20 per tool.

The command answers the plan's id, which webhooks report as plan: default for the tool's first plan, else monthly or yearly, with a suffix when that one is taken.

Prices cannot be edited. To change an amount, add a plan and retire the old one: subscribers keep paying their price until they cancel or switch. After you add the tool's first subscription plan, run npx -y toolaby@latest upgrade <tool-id>, then rebuild and reload.

2. Subscribe on Test

With Nothing free under Access, the Toolaby popup shows the plan to buy, with one button that opens its checkout. With Free uses, it shows the plans once the device has spent its uses. Select Subscribe:

  1. The checkout page opens with that plan chosen. A plan with a trial says so: 7 days free, then renews every month. Cancel any time. A yearly plan is marked with its saving against twelve months of the monthly one.
  2. The buyer selects the plan's button and signs in, with a one-time link by email or Continue with Google. A subscription always belongs to an account.
  3. Payment runs on Stripe's page, on your Stripe account, in the buyer's language. On Test, pay with 4242 4242 4242 4242, any future date and any CVC.
  4. The after-purchase page signs the extension in to the buyer's account. The device unlocks, and onPaid fires in the background.

When it worked, the buyer appears on the tool's Customers page, with Renews and a date, or Trial until in a trial.

3. Read the subscription in your extension

gate() and getUser() answer from the device. While the subscription is trialing or active, gate() answers { allowed: true, isPremium: true, unlimited: true }, with the plan's features. In any other status it answers as the Free plan does: with Nothing free, { allowed: false, reason: 'paid' }.

getUser() describes the subscription whatever its status:

trialing, activepast_due, unpaid, canceled
user.paidtruefalse
user.via'subscription'null
user.planThe plan heldnull
user.subscription{ status, periodEnd, cancelAtPeriodEnd, cancelAt }The same, with Stripe's status

periodEnd is when the current period ends; in a trial, the trial's end. cancelAtPeriodEnd is true once the subscription is set to end, and cancelAt says when. Your popup can say why the tool is locked:

import { toolaby } from './toolaby.js';

const user = await toolaby.refresh();
if (user.subscription?.status === 'past_due') {
  notice.textContent = 'Your last payment failed.';
  updateCard.addEventListener('click', () => toolaby.openAccountPage());
}

Call refresh() once when your popup opens. A device hears of a change when it next asks Toolaby: while it is in use, at most 15 minutes after the change. The Toolaby popup asks when it opens; on Live, once its last answer is five minutes old.

4. Send events to your server

Register an endpoint under Configure → Webhooks, or from the terminal:

npx -y toolaby@latest webhooks add <url> --env-file .env

It writes the endpoint's signing secret into .env as TOOLABY_WEBHOOK_SECRET. Deliveries are signed as Standard Webhooks. Verify each one with the svix package:

import { Webhook } from 'svix';

export async function POST(req: Request) {
  const body = await req.text(); // the raw body, not parsed
  const event = new Webhook(process.env.TOOLABY_WEBHOOK_SECRET).verify(body, {
    'webhook-id': req.headers.get('webhook-id')!,
    'webhook-timestamp': req.headers.get('webhook-timestamp')!,
    'webhook-signature': req.headers.get('webhook-signature')!,
  }); // throws when the signature is wrong, or the timestamp older than five minutes
  …
}

Answer 2xx within a few seconds and do the work after; any other answer, or a timeout, is retried. Use the event's id for idempotency, and apply events by created: they can arrive out of order.

MomentEvent
Subscribed, with or without a trialpurchase.completed and subscription.started
Set to end at the period's endsubscription.cancelled, immediately: false
Endedsubscription.cancelled, immediately: true
A payment disputedpayment.disputed, then dispute.closed
A trial ends, a renewal, a payment fails, a failed payment is paid, a plan switched, a payment refundednone

subscription.cancelled carries ends_at, when access ends, and immediately, whether it already has. A server that needs to know who is paid, past due included, asks the API. Retrieve a customer answers entitled and the subscription's status.

To try it on your machine, with no endpoint registered and no tunnel:

npx -y toolaby@latest webhooks trigger subscription.cancelled --to http://localhost:4000/webhooks

It signs with your machine's development secret, which npx -y toolaby@latest webhooks secret prints.

Renewals

Each period, Stripe charges the buyer's card. Paid, the subscription stays active, and user.subscription.periodEnd, user.paidAt and the Renews date under Customers move on. When a trial ends, Stripe charges the card too. Paid, the subscription is active; refused, it is past_due.

A subscriber switches between your subscription plans from their account page, through Stripe's billing portal. The difference is prorated and invoiced at once.

On the Hobby plan, each charge carries Toolaby's fee. Toolaby costs 0.5% of a sale, or $25 a month per workspace. See Money.

Failed payments

A charge Stripe cannot make, such as a declined or expired card, leaves the subscription past_due while Stripe retries it. past_due unlocks nothing:

  • gate() answers as the Free plan does, and getUser() answers paid: false with subscription.status: 'past_due'.
  • Where the Free plan does not let the device through, the Toolaby popup says Payment failed — update your card. It shows the way to the account page.
  • The account bar shows the failed payment, with Update card as its filled button.
  • Customers shows Past due, under Needs attention.

When a later attempt pays, the subscription is active again, and the device unlocks at its next check. When Stripe's retries run out, Stripe cancels the subscription, marks it unpaid or leaves it past_due. Your Stripe account's settings for failed payments decide which.

Cancellation and the account page

toolaby.openAccountPage() opens the buyer's account page. Under Purchases, Change plan lists your other plans, each with Switch. The card, invoices and cancelling are one line at the foot, in Stripe's portal.

The buyer cancels there, or you select Cancel on the customer's page with At the end of the period. The subscription is then set to end:

  • It stays active, and unlocks until the period ends.
  • getUser() answers subscription.cancelAtPeriodEnd: true, and cancelAt, the day it ends.
  • Customers shows Ends and the date, under Needs attention.
  • Toolaby sends subscription.cancelled, with immediately: false.

Keep, on the customer's page, undoes the cancellation before the period ends. At the period's end, or at once with Cancel and Now, the subscription is canceled, and gate() answers as the Free plan does.

A refund does not end a subscription. On Live, refund its payment in your Stripe Dashboard: the subscription runs on and unlocks until you cancel it.

Next steps

  • Subscriptions — every status, with refunds and disputes.
  • Pricing — device counts, seats, coupons and retiring a plan.
  • Webhooks — every event's fields, delivery order and retries.
  • Ship to the Chrome Web Store — connect Stripe on Live and publish the store build.
  • Testing — purchases that fail, renew or are refunded.

On this page