# Recipe: License keys

> Sell a desktop app, CLI, plugin or template with license keys. The shipped app checks its key with no secret inside.

Perly Pay issues a key when the payment is fulfilled, counts activations per device, and answers "is this key good for this product here?" through public endpoints. The software you ship uses `LicenseClient`, which needs no secret key.

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

## How it works

1. **A product that issues keys** — `issues_license: true` and `license_activation_limit` (devices per key), with a one-time, lifetime, fixed-term or subscription price.
2. **Sell it** — A [payment link](https://pay.perly.io/docs/payment-links.md) on your site, or an invoice per buyer.
3. **Send the key** — On the `license.created` webhook (its object is the key), or `licenseKeys.list({ customer })` after `invoice.fulfilled`. The receipt does not carry it.
4. **Activate in the app** — `activate({ key, product, instance_name })` with a name that stays the same on this device; keep the activation id.
5. **Check on start** — `validate({ key, product, instance })`. A definite `valid: false` locks; no network keeps the last good answer for a few days.

**Node**

```js
import { LicenseClient } from "perly-pay";

const licenses = new LicenseClient(); // no secret key, ever
const result = await licenses.activate({ key, product: "prod_…", instance_name: deviceName });
if (!result.valid) showWhy(result.reason); // limit_reached, revoked, expired, …
```

**PHP**

```php
use Perly\Pay\LicenseClient;

$result = (new LicenseClient())->activate(['key' => $key, 'product' => 'prod_…', 'instance_name' => home_url()]);
if (!$result['valid']) echo $result['reason'];
```

## Run the example

1. **Install** — `npm install` in `examples/license-keys` (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** — `npm run buy-test -- you@example.com` buys a copy in test mode and prints the key; then `npm run app -- activate <key>` and `npm run app -- pro hello`.

## Pitfalls

- A `sk_…` key inside the app. Anyone can pull it out; shipped code uses `LicenseClient` only.
- Leaving out `product`: then a key for anything you sell unlocks this.
- An `instance_name` that changes every run: each start eats an activation.
