# 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`.

> **Note:** 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. **A plan** — A product and a price with `model: "subscription"` and `recurring.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_access` and `ends_at`, on the server, on every visit or with a short cache.
4. **Listen** — `invoice.fulfilled` for the first payment and each renewal; `subscription.past_due`, `subscription.unpaid` and `subscription.ended` to warn and lock.
5. **Let buyers manage it** — A customer portal link (`portalSessions.create`) to pay the open renewal, prepay or cancel.

**Subscribe, then gate**

```js
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](https://pay.perly.io/docs/subscriptions.md) for grace days, plan changes and statuses.

## Run the example

1. **Install** — `npm install` in `examples/saas-subscription` (Node 20.12+).
2. **Key** — Copy `.env.example` to `.env` and put a test key (`sk_test_…`) in `PERLY_PAY_SECRET_KEY`.
3. **Set up** — `npm run setup` makes the product and price with your key and prints their ids for `.env`.
4. **Run** — `npm start` (or `npm 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.fulfilled` webhook or `entitlements.check` from your server means paid.
