license-keys — скилл для Claude Code: что делает и как установить
Настраивает проверку лицензионных ключей Dodo Payments в приложениях и CLI. Помогает ограничить число активаций на пользователя или устройство.
npx skills add dodopayments/skills --skill license-keys -a claude-codeЧто делает license-keys
Скилл обучает агента работать с SDK Dodo Payments для управления лицензиями ПО. Он описывает разделение на клиентские и серверные методы. Для активации и проверки ключей на стороне пользователя через client.licenses.* секретный ключ не нужен. SDK принимает фиктивный токен вроде public. Это защищает мастер-ключ от утечки в скомпилированных бинарниках десктопных программ.
Для бэкенда используются методы client.entitlements.grants.*. Старые вызовы client.licenseKeys.list() и retrieve() уже объявлены устаревшими. При превышении лимита активаций API возвращает ошибку 422. Подписочные ключи не требуют ручной блокировки: сервис сам отзывает их при смене тарифа или возврате средств.
Какие задачи решает
- внедрить активацию и валидацию ключей через
client.licensesв десктопном приложении - отслеживать лимиты привязанных устройств и обрабатывать ошибку
422 - настроить чтение выданных ключей через эндпоинты
entitlements.grants - связать доступ к функциям со статусом подписки Dodo Payments
Примеры запросов агенту
- Напиши функцию активации лицензии Dodo Payments для Electron-приложения с сохранением
instanceId - Покажи, как проверять лимит активаций ключа и обрабатывать ошибку 422 в TypeScript
- Сделай серверный запрос списка ключей клиента через
client.customers.listEntitlementGrants
Как установить license-keys
1Через Skills CLI (Vercel)
npx skills add dodopayments/skills --skill license-keys -a claude-code2Вручную: положить файлы
~/.claude/skills/license-keys/SKILL.mdПапка скилла целиком; для одного проекта — .claude/skills/license-keys/
1Через Skills CLI (Vercel)
npx skills add dodopayments/skills --skill license-keys -a codex2Вручную: положить файлы
.agents/skills/license-keys/SKILL.mdДля всех проектов — ~/.agents/skills/
1Через Skills CLI (Vercel)
npx skills add dodopayments/skills --skill license-keys -a cursor2Вручную: положить файлы
.cursor/skills/license-keys/SKILL.mdCursor читает и .agents/skills/, и .claude/skills/
1GitHub CLI
gh skill install dodopayments/skills license-keys2Вручную: положить файлы
.github/skills/license-keys/SKILL.md1Через Skills CLI (Vercel)
npx skills add dodopayments/skills --skill license-keys -a gemini-cli2Командой платформы
gemini skills install https://github.com/dodopayments/skills --path dodo-payments/license-keysВручную: положить файлы
.devin/skills/license-keys/SKILL.mdСтарый путь .windsurf/skills/ тоже читается
Через Skills CLI (Vercel)
npx skills add dodopayments/skills --skill license-keys -a opencodeClaude.ai
Упакуйте папку в zip и загрузите в Settings → Capabilities → Skills
По шагам, с командами для каждой платформы: Как установить скилл в Claude Code: пять способов
Вопросы о license-keys
Как установить license-keys в Claude Code?
Выполните в терминале команду npx skills add dodopayments/skills --skill license-keys -a claude-code. Файл навыка сохранится по пути ~/.claude/skills/license-keys/SKILL.md.
Что делает этот скилл?
Он даёт агенту инструкции и примеры кода на TypeScript для работы с ключами лицензий Dodo Payments. Скилл охватывает активацию устройств, валидацию без секретного ключа и серверное управление через гранты.
Безопасно ли использовать методы лицензий в клиентском коде?
Да, роуты client.licenses.* публичные и не требуют передачи секретного ключа API. В конструктор SDK передаётся плейсхолдер вроде public, поэтому секрет не попадёт в приложение.
Исходный текст
SKILL.md · MIT
---
name: license-keys
description: Dodo Payments license keys for software products, covering activation, validation, and deactivation via client.licenses, activation limits, expiry, entitlement grants, revocation, and license webhooks. Use when gating a desktop app, CLI, plugin, or SaaS feature behind a license key, enforcing per-seat or per-device limits, or building a license dashboard.
---
# Dodo Payments License Keys
License keys authorize access to your digital products. Use them for software licensing, per-seat controls, and gating premium features.
## When to use this skill
- Implementing end-user license activation and validation flows
- Building merchant dashboards to manage customer license keys
- Handling license expiry, activation limits, and subscription-linked revocation
- Integrating license checks into desktop apps, CLIs, or web services
- Reacting to license lifecycle webhooks
## Core concepts
**License Key Entitlements** deliver keys when a product is purchased. The entitlement config supplies activation limits, optional duration, and fulfillment mode (auto or manual).
**Three distinct resources:**
1. **`client.licenses.*`** — end-user activation flow (public, no API key required)
- `activate()` — activate a key on a device
- `validate()` — check if a key is valid
- `deactivate()` — free an activation slot
2. **Entitlement grants** — merchant-side reads and revocation (requires API key)
- `client.customers.listEntitlementGrants()`, `client.entitlements.grants.list()` — list issued keys (each license-key grant carries a `license_key` object)
- `client.entitlements.grants.revoke()` — revoke a key
- `client.licenseKeys.create()` — still supported, for **importing** existing keys
- `client.licenseKeys.list()` / `retrieve()` / `update()` are **deprecated** (`GET /license_keys`, `GET`/`PATCH /license_keys/{id}`); use the grant endpoints above instead
3. **`client.licenseKeyInstances.*`** — per-device activation instances (requires API key)
- `list()`, `retrieve()`, `update()` — track active devices per key
**Activation limits:** blank/null means unlimited; otherwise it's the maximum concurrent activations. Attempting to activate beyond the limit returns `422`.
**Expiry semantics:**
- One-time-payment keys honor the entitlement duration.
- Subscription-issued keys have no independent expiry; Dodo drives their validity from subscription state **automatically**: `past_due` leaves them active, `on_hold` and `paused` disable them until the subscription recovers or resumes, `cancelled`/`expired` disable them permanently, and `plan_changed` replaces them. A one-time `refund.succeeded` also disables them. You do not need to disable keys yourself.
- Imported keys use nullable `expires_at`; null means perpetual.
---
## End-User Activation Flow
### Activate a license key
```typescript
import DodoPayments from 'dodopayments';
// The license endpoints are public and don't check the token, but the SDK
// constructor requires a value. Never ship your secret API key in client software.
// Set the environment explicitly: the SDK defaults to live_mode.
const client = new DodoPayments({ bearerToken: 'public', environment: 'test_mode' });
async function activateLicense(licenseKey: string, deviceName: string) {
try {
const response = await client.licenses.activate({
license_key: licenseKey,
name: deviceName, // e.g., "John's MacBook Pro"
});
return {
success: true,
instanceId: response.id,
customerId: response.customer.customer_id,
productId: response.product.product_id,
};
} catch (error: any) {
if (error.status === 422) {
return { success: false, error: 'Activation limit reached' };
}
return { success: false, error: error.message || 'Activation failed' };
}
}
```
Response includes `id` (instance ID), `business_id`, `name`, `license_key_id`, `created_at`, customer details, and product details.
### Validate a license key
```typescript
import DodoPayments from 'dodopayments';
const client = new DodoPayments({ bearerToken: 'public', environment: 'test_mode' }); // public endpoints: placeholder token; use 'live_mode' in production
async function validateLicense(licenseKey: string, instanceId: string) {
try {
const response = await client.licenses.validate({
license_key: licenseKey,
// Also checks that THIS device's activation still exists, so a
// deactivated device stops validating even though the key is active.
license_key_instance_id: instanceId,
});
return { valid: response.valid };
} catch (error) {
return { valid: false };
}
}
```
Without `license_key_instance_id`, validation only checks that the key is `active` and unexpired. Pass the instance ID you stored at activation.
### Deactivate a license
```typescript
import DodoPayments from 'dodopayments';
const client = new DodoPayments({ bearerToken: 'public', environment: 'test_mode' }); // public endpoints: placeholder token; use 'live_mode' in production
async function deactivateLicense(licenseKey: string, instanceId: string) {
try {
await client.licenses.deactivate({
license_key: licenseKey,
license_key_instance_id: instanceId,
});
return { success: true };
} catch (error: any) {
return { success: false, error: error.message };
}
}
```
---
## Merchant-Side Key Management
### List a customer's license keys
`client.licenseKeys.list()`, `retrieve()`, and `update()` are deprecated. Read keys through the entitlement grant endpoints; each license-key grant carries a `license_key` object (`key`, `status`, `expires_at`, `activations_used`, `activations_limit`), which is `null` on a manual-mode grant still `Pending`.
```typescript
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
});
async function listCustomerKeys(customerId: string) {
const keys = [];
for await (const grant of client.customers.listEntitlementGrants(customerId, {
integration_type: 'license_key',
status: 'Delivered',
})) {
if (!grant.license_key) continue;
keys.push({
grantId: grant.id,
entitlementId: grant.entitlement_id,
key: grant.license_key.key,
status: grant.license_key.status,
expiresAt: grant.license_key.expires_at,
activationsUsed: grant.license_key.activations_used,
activationsLimit: grant.license_key.activations_limit,
});
}
return keys;
}
```
To list every key issued for one License Key entitlement, use `client.entitlements.grants.list('ent_...', { status: 'Delivered' })`.
### Revoke a license key
```typescript
// Disables the key with revocation_reason: manual. Manually revoked keys are
// not re-granted on subscription renewal.
await client.entitlements.grants.revoke('entg_abc123', { id: 'ent_license_key_id' });
```
### Import a license key (`POST /license_keys`)
Use this to migrate keys from another system. Imported keys do **not** trigger a customer email; notify the customer yourself. For keys Dodo issues, Dodo emails them.
```typescript
const newKey = await client.licenseKeys.create({
customer_id: 'cus_abc123',
key: 'PREMIUM-AAAA-BBBB-CCCC',
product_id: 'pdt_abc123',
activations_limit: 5,
expires_at: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
});
console.log(newKey.key); // The actual license key string
```
---
## List Activation Instances
```typescript
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
environment: 'test_mode',
});
// List all instances for a specific license key
for await (const instance of client.licenseKeyInstances.list({
license_key_id: 'lk_abc123',
})) {
console.log({
id: instance.id,
name: instance.name,
createdAt: instance.created_at,
});
}
```
Optional query fields: `page_size` (default 10, max 100), `page_number` (default 0), `license_key_id`, and `grant_id`.
---
## Desktop App Integration
Full guide: [references/desktop-app.md](references/desktop-app.md).
Covers:
- Electron app example
- React component for license input
## CLI Tool Integration
Full guide: [references/cli-tool.md](references/cli-tool.md).
Covers:
- Node.js CLI example
- CLI commands
## Webhook Integration
### Handle license key delivery
Dodo generates **and emails** each auto-fulfilled key to the customer (including the entitlement's activation message), and it disables, re-enables, and replaces keys as the subscription changes state. Your webhook only needs to mirror that state for your own records - do not email keys yourself and do not disable keys on cancellation.
When a product with licensing enabled is purchased, an auto-fulfilled key arrives as `entitlement_grant.created` with `status: "Delivered"` and a `license_key` object; no separate `entitlement_grant.delivered` follows. `entitlement_grant.delivered` fires only when a grant moves to `Delivered` later (for example, when you manually fulfill a `Pending` grant, or a revoked grant is restored), so handle both:
```typescript
// app/api/webhooks/dodo/route.ts
import { NextRequest, NextResponse } from 'next/server';
import DodoPayments from 'dodopayments';
const client = new DodoPayments({
bearerToken: process.env.DODO_PAYMENTS_API_KEY,
webhookKey: process.env.DODO_PAYMENTS_WEBHOOK_KEY,
environment: process.env.DODO_PAYMENTS_ENVIRONMENT === 'live_mode' ? 'live_mode' : 'test_mode',
});
export async function POST(req: NextRequest) {
try {
const body = await req.text();
const event = client.webhooks.unwrap(body, {
headers: {
'webhook-id': req.headers.get('webhook-id') || '',
'webhook-signature': req.headers.get('webhook-signature') || '',
'webhook-timestamp': req.headers.get('webhook-timestamp') || '',
},
});
if (
(event.type === 'entitlement_grant.created' || event.type === 'entitlement_grant.delivered') &&
event.data.status === 'Delivered' &&
event.data.license_key // other entitlement types (and Pending manual grants) have no key
) {
// Dodo emails the key to the customer itself, so there is no email to send here.
await mirrorGrant(event.data, 'active');
}
if (event.type === 'entitlement_grant.revoked') {
// Dodo already disabled the key (cancellation, expiry, refund, on_hold,
// pause, plan change, or manual revoke). Mirror it locally.
await mirrorGrant(event.data, 'revoked');
}
return NextResponse.json({ received: true });
} catch (error) {
console.error('Webhook error:', error);
return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
}
}
type Grant = DodoPayments.Entitlements.EntitlementGrant;
// Webhooks can arrive out of order and be retried concurrently. Apply a grant
// only when it is newer than the stored copy (by the grant's own updated_at),
// using a conditional UPDATE so a stale "delivered" can never overwrite a newer
// "revoked". If no row exists yet - including a revocation that arrives before
// the delivery - insert one; the unique externalId keeps concurrent inserts
// safe, and the loser retries the conditional update so the newest grant wins.
async function mirrorGrant(grant: Grant, status: 'active' | 'revoked') {
const grantUpdatedAt = new Date(grant.updated_at);
const fields = {
customerId: grant.customer_id,
status,
grantUpdatedAt,
...(grant.license_key && {
key: grant.license_key.key,
expiresAt: grant.license_key.expires_at ? new Date(grant.license_key.expires_at) : null,
activationsLimit: grant.license_key.activations_limit,
}),
};
const applyIfNewer = () =>
prisma.license.updateMany({
where: { externalId: grant.id, grantUpdatedAt: { lt: grantUpdatedAt } },
data: fields,
});
const updated = await applyIfNewer();
if (updated.count > 0) return;
// Either no row yet, or the stored copy is already as new or newer.
const inserted = await prisma.license.createMany({
data: [{ externalId: grant.id, ...fields }],
skipDuplicates: true,
});
if (inserted.count === 0) {
// A concurrent webhook inserted the row first. It may hold an OLDER grant,
// so re-run the conditional update; it is a no-op if the row is newer.
await applyIfNewer();
}
}
```
**Webhook events:**
- `entitlement_grant.created` — grant created; `status: "Delivered"` with a `license_key` for auto-fulfilled keys (the primary issuance event), `status: "Pending"` with no key for manual fulfillment
- `entitlement_grant.delivered` — an existing grant moved to `Delivered` (manual fulfillment, or a revoked grant restored); not sent for auto-fulfilled keys
- `entitlement_grant.revoked` — license key revoked (Dodo already disabled it; mirror the state, inspect `revocation_reason`)
- `license_key.created` — legacy event (still fires, but use `entitlement_grant.*` for new integrations)
Subscription `cancelled`/`expired`/`on_hold`/`paused` events need no license action from you: Dodo disables the keys itself, and `licenses.validate()` reflects it immediately.
---
## Common Mistakes
### 1. Validating only at install time
Don't validate once and trust forever. Validate periodically (e.g., weekly) to catch revoked or expired keys.
```typescript
// Bad: validate once
if (await validateLicense(key)) {
store.set('trusted', true);
}
// Good: validate on each startup
const isValid = await validateLicense(key);
if (!isValid) {
// Fail closed
process.exit(1);
}
```
### 2. Not handling activation-limit errors
When a user hits the activation limit, they need a clear path to deactivate an old device.
```typescript
try {
await client.licenses.activate({ license_key: key, name: deviceName });
} catch (error: any) {
if (error.status === 422) {
// Show UI: "You've reached your activation limit. Deactivate a device first."
// Provide a list of active instances so they can choose which to remove.
}
}
```
### 3. Trusting client-side validation alone
Always validate on your server before granting access to sensitive features.
```typescript
// Bad: trust the client
if (localStorage.getItem('license_valid')) {
showPremiumFeature();
}
// Good: validate server-side
const response = await fetch('/api/validate-license', {
method: 'POST',
body: JSON.stringify({ licenseKey }),
});
const { valid } = await response.json();
if (valid) {
showPremiumFeature();
}
```
### 4. Hardcoding license keys
Never embed keys in client code or version control.
```typescript
// Bad
const LICENSE_KEY = 'PRO-AAAA-BBBB-CCCC-DDDD';
// Good
const licenseKey = process.env.DODO_LICENSE_KEY;
// or read from user input / secure storage
```
### 5. Not storing the instance ID
The instance ID is required for deactivation. Store it alongside the key.
```typescript
// Bad: only store the key
store.set('license_key', key);
// Good: store both
store.set('license', {
key,
instanceId: response.id,
activatedAt: new Date().toISOString(),
lastValidatedAt: new Date().toISOString(),
});
```
### 6. Failing open on validation errors
If validation fails (network error, server down), fail closed. Don't grant access.
```typescript
// Bad: assume valid if offline
try {
const valid = await validateLicense(key);
return valid;
} catch {
return true; // WRONG: grants access on error
}
// Good: fail closed, with optional grace period
try {
const valid = await validateLicense(key);
return valid;
} catch {
// Only trust local cache if within grace period
const lastValidated = store.get('last_validated_at');
const daysSince = (Date.now() - lastValidated) / (1000 * 60 * 60 * 24);
return daysSince < 30;
}
```
---
## Resources
- [License Keys Guide](https://docs.dodopayments.com/features/license-keys)
- [Activate License API](https://docs.dodopayments.com/api-reference/licenses/activate-license)
- [Validate License API](https://docs.dodopayments.com/api-reference/licenses/validate-license)
- [Deactivate License API](https://docs.dodopayments.com/api-reference/licenses/deactivate-license)
- [License Key Instances API](https://docs.dodopayments.com/api-reference/licenses/get-license-key-instances)
- [List Customer Grants API](https://docs.dodopayments.com/api-reference/entitlements/list-customer-grants)
- [Entitlement Grant Webhooks](https://docs.dodopayments.com/developer-resources/webhooks/intents/entitlement-grant)
- [Webhook Verification](https://docs.dodopayments.com/developer-resources/webhooks/verification)
© dodopayments · MIT · dodopayments/skills