Skip to content

Guides

Recipe: SaaS subscription

A monthly or yearly plan paid in crypto. Every period is an invoice with reminders; your app asks Perly Pay who has access.

View as Markdown

Crypto cannot be pulled from a wallet, so a subscription renews by invoice: before each period ends the buyer gets the next one, then reminders until it is paid. There is no auto-debit. Your app never counts days: it asks entitlements.check.

With an assistant: the sell-subscription skill of the Perly Pay plugin for Claude Code does this recipe in your project — the plugin and its example, examples/saas-subscription (MIT), are on their way. Until then, any assistant can read the same skill from /.well-known/skills/perly-pay-sell-subscription/SKILL.md.

How it works

  1. 1

    A plan

    A product and a price with model: "subscription" and recurring.interval (month, year…), optional trial and grace days.

  2. 2

    Subscribe

    An invoice for that price with the user as customer opens the subscription and its first invoice. Paying it makes the plan active.

  3. 3

    Gate features

    entitlements.check({ customer_external_id, product }) → has_access and ends_at, on the server, on every visit or with a short cache.

  4. 4

    Listen

    invoice.fulfilled for the first payment and each renewal; subscription.past_due, subscription.unpaid and subscription.ended to warn and lock.

  5. 5

    Let buyers manage it

    A customer portal link (portalSessions.create) to pay the open renewal, prepay or cancel.

Subscribe, then gate
const invoice = await pay.invoices.create(
  { line_items: [{ price: PRO_MONTHLY }], customer: customer.id, metadata: { user: userId }, return_url },
  { idempotencyKey: `subscribe-${userId}-${today}` },
);
redirect(invoice.url); // invoice.subscription is the new subscription

const { has_access } = await pay.entitlements.check({ customer_external_id: userId, product: PRO });

See Subscriptions and reminders for grace days, plan changes and statuses.

Run the example

  1. 1

    Install

    npm install in examples/saas-subscription (Node 20.12+).

  2. 2

    Key

    Copy .env.example to .env and put a test key (sk_test_…) in PERLY_PAY_SECRET_KEY.

  3. 3

    Set up

    npm run setup makes the product and price with your key and prints their ids for .env.

  4. 4

    Run

    npm start (or npm run dev), as its README says.

  5. 5

    Pay without a wallet

    Subscribe on the home page, press Simulate payment on the invoice, then open /pro?user=you@example.com: Pro is unlocked until the period's end.

Pitfalls

  • Saying "we charge your wallet automatically". Nothing is pulled; say "renews by invoice".
  • A missing or fake customer email: renewal invoices and reminders go there.
  • Locking at the first late minute. Decide what past_due (inside grace days) means for access.
  • Granting access on the redirect back from checkout. Only a verified invoice.fulfilled webhook or entitlements.check from your server means paid.