EYMINT
DocumentationAPI Reference
BlogDashboard
EYMINT
SectionsDocumentationAPI Reference

Getting Started

  • Keymint Documentation
  • Introduction
  • Getting Started
  • License Keys
  • Changelog

SDK References

  • Software Development Kits (SDKs)
  • Node.js SDK
  • Go SDK
  • Python SDK
  • C# SDK

Integration Guides

  • Integration Guides
  • Customer Portal for Vendors
  • Roles & Permissions
  • Radar Alert Destinations
  • Payment Provider Integrations
  • Stripe Subscription Licensing
  • Email Delivery
  • API Key Management
  • Webhook Deliveries
  • Analytics and Retention
  • Dashboard Workflows
  • Electron Licensing
  • PyQt Licensing
  • Tauri Licensing
  • Next.js SaaS Licensing

Licensing Guides

  • Licensing Concepts
  • Node-Locking & Machine Identifiers
  • Floating Licenses
  • Offline Verification
  • License Radar
  • Public Key Management and Rotation
  • Feature Flags
  • Webhooks & Events

Templates

  • Starter Templates
  • Electron License Starter
  • PyQt License Starter
  • Tauri License Starter
  • Next.js SaaS License Starter

Security & Trust

  • Security & Trust

Support

  • Frequently Asked Questions
  • Customer Portal
  • Using the Customer Portal
  • Workspace Settings and Roles

Legal

  • Privacy Policy
  • Subprocessors
  • Data Processing Agreement
  • Security Overview
  • Terms of Service
  • Service Level Agreement (SLA)
  • GDPR Compliance
Go to Dashboard

Getting Started

  • Keymint Documentation
  • Introduction
  • Getting Started
  • License Keys
  • Changelog

SDK References

  • Software Development Kits (SDKs)
  • Node.js SDK
  • Go SDK
  • Python SDK
  • C# SDK

Integration Guides

  • Integration Guides
  • Customer Portal for Vendors
  • Roles & Permissions
  • Radar Alert Destinations
  • Payment Provider Integrations
  • Stripe Subscription Licensing
  • Email Delivery
  • API Key Management
  • Webhook Deliveries
  • Analytics and Retention
  • Dashboard Workflows
  • Electron Licensing
  • PyQt Licensing
  • Tauri Licensing
  • Next.js SaaS Licensing

Licensing Guides

  • Licensing Concepts
  • Node-Locking & Machine Identifiers
  • Floating Licenses
  • Offline Verification
  • License Radar
  • Public Key Management and Rotation
  • Feature Flags
  • Webhooks & Events

Templates

  • Starter Templates
  • Electron License Starter
  • PyQt License Starter
  • Tauri License Starter
  • Next.js SaaS License Starter

Security & Trust

  • Security & Trust

Support

  • Frequently Asked Questions
  • Customer Portal
  • Using the Customer Portal
  • Workspace Settings and Roles

Legal

  • Privacy Policy
  • Subprocessors
  • Data Processing Agreement
  • Security Overview
  • Terms of Service
  • Service Level Agreement (SLA)
  • GDPR Compliance

Stripe Subscription Licensing

Sell subscription-based software licenses with Stripe and Keymint. Every subscription payment automatically provisions and manages license keys.

Problem

You sell a SaaS or desktop application with monthly/annual subscriptions via Stripe. You need every paid subscription to automatically generate a license key, and you need cancelled subscriptions to revoke access. Building this plumbing from scratch means maintaining Stripe webhooks, a license database, rate-limiting, offline validation, and customer-facing activation flows. Keymint handles the licensing layer while Stripe handles billing.

Keymint has no built-in Stripe connection. Your own Stripe webhook handler, shown below, calls the Keymint API to create, extend and block licenses.

Architecture Diagram

mermaid
Rendering diagram...

Create Product

First, create a product in Keymint. This groups all licenses under one software title.

  1. Sign in to the Keymint Dashboard
  2. Navigate to Products
  3. Enter a name (e.g. "My Desktop App")
  4. Copy the product ID — you will use it for all license operations.

Create License Policy

When generating a license key, save your settings as a Template (in More options, open Save as a template) with these defaults:

  • maxActivations: 1 (one device per subscriber)
  • licenseType: "node-locked" (tied to a specific machine)
  • expiryDate: Match the Stripe subscription period end

Issue License

When Stripe confirms the first payment of a subscription, your backend creates a license key and emails it to the customer. Renewals extend the existing key instead (see the complete source below).

These samples use invoice.subscription and subscription.current_period_end, which exist in stripe-node 17 and earlier. From Stripe API version 2025-03-31 (stripe-node 18) they moved to invoice.parent.subscription_details.subscription and the subscription items' current_period_end. Adjust for the API version your account uses.

typescript
import { NextRequest } from "next/server";
import Stripe from "stripe";
import { KeyMint } from "keymint";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const keymint = new KeyMint(process.env.KEYMINT_ADMIN_API_KEY!);

export async function POST(req: NextRequest) {
  const payload = await req.text();
  const sig = req.headers.get("stripe-signature")!;

  const event = stripe.webhooks.constructEvent(
    payload,
    sig,
    process.env.STRIPE_WEBHOOK_SECRET!
  );

  if (event.type === "invoice.paid") {
    const invoice = event.data.object as Stripe.Invoice;
    // Only the first invoice issues a key; renewals extend it.
    if (!invoice.subscription || invoice.billing_reason !== "subscription_create") {
      return Response.json({ received: true });
    }

    const subscription = await stripe.subscriptions.retrieve(
      invoice.subscription as string
    );

    const customer = await stripe.customers.retrieve(invoice.customer as string);
    const customerEmail = invoice.customer_email
      || ("deleted" in customer ? null : customer.email);

    // Create a license key that expires at the end of the billing period.
    // The idempotency key stops a redelivered event from creating a second key.
    const license = await keymint.createKey({
      productId: process.env.KEYMINT_PRODUCT_ID!,
      maxActivations: "1",
      expiryDate: new Date(subscription.current_period_end * 1000).toISOString(),
      newCustomer: {
        name: customerEmail ?? "Customer",
        email: customerEmail ?? undefined,
      },
    }, { idempotencyKey: `stripe-${event.id}` });

    // Send the license key to your customer
    if (customerEmail && license.key) {
      await sendLicenseEmail(customerEmail, license.key);
    }
  }

  return Response.json({ received: true });
}

What this does:

  1. Verifies the Stripe webhook signature (required for security)
  2. On the subscription's first invoice.paid, retrieves the subscription details
  3. Creates a license key scoped to the Stripe subscription period
  4. Auto-creates a customer record and assigns the license
  5. Emails the raw license key to your customer

The raw license key is returned only at creation and is encrypted at rest. Store it (mapped to the Stripe customer and subscription) before emailing it: the API can't look a key up without already knowing it. You can still view or export keys in the dashboard.

Validate License

Your desktop app validates the license on startup using a client-scoped API key.

typescript
import { KeyMint } from 'keymint';

const client = new KeyMint(process.env.KEYMINT_CLIENT_API_KEY!);

// 'unknown' = Keymint couldn't be reached; keep the last known state.
async function validateLicense(licenseKey: string): Promise<'valid' | 'rejected' | 'unknown'> {
  const hostId = KeyMint.getOrCreateInstallationId(); // Stable per-machine ID

  try {
    const result = await client.activateKey({
      productId: process.env.KEYMINT_PRODUCT_ID!,
      licenseKey,
      hostId,
    });

    console.log(`License valid (${result.message})`); // "License valid"
    return 'valid';
  } catch (error: any) {
    // The SDK throws on every non-2xx response.
    if ([401, 402, 403, 404].includes(error?.status)) {
      console.error(`License rejected: ${error.errorCode}`);
      return 'rejected';
    }
    console.error('Could not reach Keymint:', error?.message);
    return 'unknown';
  }
}

First activation: The hostId is recorded. Subsequent startups on the same machine return success immediately without consuming another activation slot.

Handle Activations

Each activation consumes one seat. The maxActivations limit on the license key controls how many devices can activate.

ScenarioBehavior
First activationLicense becomes activated: true. activations count increments to 1.
Same host reactivatesReturns success. No new activation record.
New host activatesCount increments if under maxActivations.
Limit reachedReturns 403, code: 2, errorCode: "activation_limit_reached".
License expiredReturns 401, code: 1, errorCode: "license_invalid_or_forbidden". Expired licenses reject all activations.
License blockedReturns 401, code: 1, errorCode: "license_invalid_or_forbidden". Blocked licenses reject all activations.

To check current activation count programmatically:

typescript
// Requires an admin or read-only API key, such as the admin `keymint` client above
const { data } = await keymint.getKey({ productId: process.env.KEYMINT_PRODUCT_ID!, licenseKey });
console.log(`Activations: ${data.license.activations}/${data.license.maxActivations ?? 'unlimited'}`);

Handle Revocations

When a subscription ends, block the license to prevent further activations. Active devices will fail on their next validation check.

typescript
if (event.type === "customer.subscription.deleted") {
  const subscription = event.data.object as Stripe.Subscription;

  // Find the license key associated with this Stripe customer
  const licenseKey = await findLicenseByStripeCustomer(subscription.customer as string);

  if (licenseKey) {
    await keymint.blockKey({
      productId: process.env.KEYMINT_PRODUCT_ID!,
      licenseKey,
    });
  }
}

You can also deactivate a single device without blocking the entire license. Pass the hostId the device activated with (a client key must send it; an admin key may omit it to remove every device):

typescript
await keymint.deactivateKey({
  productId: process.env.KEYMINT_PRODUCT_ID!,
  licenseKey,
  hostId: "HOST_ID_OF_THE_DEVICE",
});

Production Considerations

Security

  • Never bundle admin API keys in your desktop app. Use client-scoped keys for runtime activation.
  • Store admin keys only in your backend environment variables.
  • Verify Stripe webhook signatures — the Stripe-Signature header must be validated to prevent spoofed events.

Stripe Webhook Events to Handle

Stripe EventKeymint Action
checkout.session.completedCreate license key, email customer
invoice.paidExtend license key expiry on renewal
invoice.payment_failedNotify customer (license still valid during dunning)
customer.subscription.updatedSync license expiry date
customer.subscription.deletedBlock license key

Keymint Webhook Events for Monitoring

Subscribe your backend to Keymint outbound webhooks for additional safety:

Keymint EventUse
license.createdLog license creation in your CRM
device.activatedTrack activations, send welcome email
license.disabledConfirm revocation, send exit survey

Rate Limits by Plan

PlanRequests/min
Free30
Maker50
Startup100
Standard1,000

Idempotency

Stripe may deliver webhooks more than once. Record each processed event.id in your own database and skip events you've already handled. Also send an Idempotency-Key when a Keymint mutation may be retried (the Node.js SDK takes it as { idempotencyKey }); without one, repeating a create request can produce another key. Keymint keeps idempotency records for 24 hours.

Complete Source Code

Full Stripe webhook handler (Next.js)
typescript
// app/api/webhooks/stripe-licensing/route.ts
import { NextRequest } from "next/server";
import Stripe from "stripe";
import { KeyMint } from "keymint";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const keymint = new KeyMint(process.env.KEYMINT_ADMIN_API_KEY!);

export async function POST(req: NextRequest) {
  const payload = await req.text();
  const sig = req.headers.get("stripe-signature")!;

  let event: Stripe.Event;
  try {
    event = stripe.webhooks.constructEvent(
      payload,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET!
    );
  } catch (err) {
    console.error("Stripe signature verification failed:", err);
    return Response.json({ error: "Invalid signature" }, { status: 400 });
  }

  try {
    switch (event.type) {
      case "checkout.session.completed": {
        const session = event.data.object as Stripe.Checkout.Session;
        if (!session.subscription) break;

        const subscription = await stripe.subscriptions.retrieve(
          session.subscription as string
        );
        await handleNewSubscription(subscription);
        break;
      }

      case "invoice.paid": {
        const invoice = event.data.object as Stripe.Invoice;
        if (!invoice.subscription) break;

        const subscription = await stripe.subscriptions.retrieve(
          invoice.subscription as string
        );
        await handleSubscriptionRenewal(subscription);
        break;
      }

      case "customer.subscription.updated": {
        const subscription = event.data.object as Stripe.Subscription;
        await handleSubscriptionChange(subscription);
        break;
      }

      case "customer.subscription.deleted": {
        const subscription = event.data.object as Stripe.Subscription;
        await handleSubscriptionCancelled(subscription);
        break;
      }
    }

    return Response.json({ received: true });
  } catch (error) {
    console.error("Webhook handler error:", error);
    return Response.json(
      { error: "Internal server error" },
      { status: 500 }
    );
  }
}

async function handleNewSubscription(subscription: Stripe.Subscription) {
  const customerEmail = await getCustomerEmail(subscription.customer as string);
  const existingLicense = await findExistingLicense(customerEmail);

  if (existingLicense) {
    await keymint.updateKey({
      productId: process.env.KEYMINT_PRODUCT_ID!,
      licenseKey: existingLicense,
      expiryDate: new Date(subscription.current_period_end * 1000).toISOString(),
    });
    return;
  }

  const license = await keymint.createKey({
    productId: process.env.KEYMINT_PRODUCT_ID!,
    maxActivations: "1",
    expiryDate: new Date(subscription.current_period_end * 1000).toISOString(),
    newCustomer: {
      name: customerEmail,
      email: customerEmail,
    },
  }, { idempotencyKey: `stripe-sub-${subscription.id}` });

  if (license.key) await sendLicenseEmail(customerEmail, license.key);
}

async function handleSubscriptionRenewal(subscription: Stripe.Subscription) {
  const customerEmail = await getCustomerEmail(subscription.customer as string);
  const licenseKey = await findExistingLicense(customerEmail);
  if (!licenseKey) return;

  await keymint.updateKey({
    productId: process.env.KEYMINT_PRODUCT_ID!,
    licenseKey,
    expiryDate: new Date(subscription.current_period_end * 1000).toISOString(),
  });
}

async function handleSubscriptionChange(subscription: Stripe.Subscription) {
  const customerEmail = await getCustomerEmail(subscription.customer as string);
  const licenseKey = await findExistingLicense(customerEmail);
  if (!licenseKey) return;

  const isActive = subscription.status === "active" || subscription.status === "trialing";
  if (isActive) {
    await keymint.unblockKey({
      productId: process.env.KEYMINT_PRODUCT_ID!,
      licenseKey,
    });
    await keymint.updateKey({
      productId: process.env.KEYMINT_PRODUCT_ID!,
      licenseKey,
      expiryDate: new Date(subscription.current_period_end * 1000).toISOString(),
    });
  } else {
    await keymint.blockKey({
      productId: process.env.KEYMINT_PRODUCT_ID!,
      licenseKey,
    });
  }
}

async function handleSubscriptionCancelled(subscription: Stripe.Subscription) {
  const customerEmail = await getCustomerEmail(subscription.customer as string);
  const licenseKey = await findExistingLicense(customerEmail);
  if (!licenseKey) return;

  await keymint.blockKey({
    productId: process.env.KEYMINT_PRODUCT_ID!,
    licenseKey,
  });
}

async function getCustomerEmail(customerId: string): Promise<string> {
  const customer = await stripe.customers.retrieve(customerId);
  if ("deleted" in customer) throw new Error("Customer deleted");
  return customer.email!;
}

// You must implement these based on your database
async function findExistingLicense(email: string): Promise<string | null> {
  // Query your DB: which license key is assigned to this Stripe customer email?
  return null;
}

async function sendLicenseEmail(email: string, licenseKey: string) {
  // Send email via SendGrid, Resend, Amazon SES, etc.
  console.log(`Sending license ${licenseKey} to ${email}`);
}
Desktop app license validation
typescript
// license.ts — bundled in your desktop app
import { KeyMint } from 'keymint';

const client = new KeyMint(process.env.KEYMINT_CLIENT_API_KEY!);
const PRODUCT_ID = process.env.KEYMINT_PRODUCT_ID!;

let cachedValidation = false;

export async function checkLicense(licenseKey: string): Promise<boolean> {
  if (cachedValidation) return true;

  try {
    await client.activateKey({
      productId: PRODUCT_ID,
      licenseKey,
      hostId: KeyMint.getOrCreateInstallationId(),
    });
    cachedValidation = true;
    return true;
  } catch (error: any) {
    // The SDK throws on every non-2xx response.
    if ([401, 402, 403, 404].includes(error?.status)) {
      console.error(`License invalid: ${error.errorCode} (HTTP ${error.status})`);
      return false;
    }
    // Offline or Keymint unavailable: decide whether to allow a grace period.
    console.error('License check failed:', error?.message);
    return false;
  }
}

// Call on app startup
export async function startLicenseCheck(licenseKey: string) {
  const valid = await checkLicense(licenseKey);
  if (!valid) {
    console.error("License check failed. Exiting.");
    process.exit(1);
  }
  console.log("License validated successfully.");
}

On this page

  • Problem
  • Architecture Diagram
  • Create Product
  • Create License Policy
  • Issue License
  • Validate License
  • Handle Activations
  • Handle Revocations
  • Production Considerations
  • Security
  • Stripe Webhook Events to Handle
  • Keymint Webhook Events for Monitoring
  • Rate Limits by Plan
  • Idempotency
  • Complete Source Code