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.
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.
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
A plan
A product and a price with
model: "subscription"andrecurring.interval(month,year…), optional trial and grace days. - 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
Gate features
entitlements.check({ customer_external_id, product })→has_accessandends_at, on the server, on every visit or with a short cache. - 4
Listen
invoice.fulfilledfor the first payment and each renewal;subscription.past_due,subscription.unpaidandsubscription.endedto warn and lock. - 5
Let buyers manage it
A customer portal link (
portalSessions.create) to pay the open renewal, prepay or cancel.
See Subscriptions and reminders for grace days, plan changes and statuses.
Run the example
- 1
Install
npm installinexamples/saas-subscription(Node 20.12+). - 2
Key
Copy
.env.exampleto.envand put a test key (sk_test_…) inPERLY_PAY_SECRET_KEY. - 3
Set up
npm run setupmakes the product and price with your key and prints their ids for.env. - 4
Run
npm start(ornpm run dev), as its README says. - 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.fulfilledwebhook orentitlements.checkfrom your server means paid.