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

Next.js SaaS Licensing

Add license key management to your Next.js SaaS. Use API routes for key creation, Server Components for validation, and webhooks to sync Stripe billing.

Problem

Your Next.js SaaS app sells to businesses that want license keys for on-premise deployments, self-hosted instances, or desktop companion apps. You need a licensing API that integrates with your existing Next.js backend, supports webhooks, and works with your Stripe billing.

Keymint handles all licensing primitives — key generation, activation counting, device binding, floating sessions — while your Next.js app owns the business logic and billing.

Architecture Diagram

mermaid
Rendering diagram...

Create Product

From the Keymint Dashboard:

  1. Products → Create Product
  2. Name: "MySaaS Self-Hosted License"
  3. Copy the product ID

Create License Policy

When generating a license key, save your settings as a Template (in More options, open Save as a template) for each SaaS tier.

TiermaxActivationslicenseTypeexpiry
Starter5node-lockedNone
Pro25node-lockedNone
Business—floating with maxConcurrentSessions: 50None

Issue License

Install the SDK in your Next.js project:

bash
npm install keymint

Create a license service module in your backend:

typescript
import { KeyMint } from 'keymint';

const admin = new KeyMint(process.env.KEYMINT_ADMIN_API_KEY!);
const PRODUCT_ID = process.env.KEYMINT_PRODUCT_ID!;

interface CreateLicenseParams {
  customerEmail: string;
  customerName: string;
  maxActivations: number;
  planName: string;
  subscriptionEndDate?: Date;
}

export async function createCustomerLicense(params: CreateLicenseParams) {
  const license = await admin.createKey({
    productId: PRODUCT_ID,
    maxActivations: String(params.maxActivations),
    expiryDate: params.subscriptionEndDate?.toISOString(),
    newCustomer: {
      name: params.customerName,
      email: params.customerEmail,
    },
    metadata: {
      plan: params.planName,
      createdBy: 'nextjs-saas',
    },
  });

  // license.key is the raw key. Store it now: the API can't return it later.
  return license;
}

export async function blockLicense(licenseKey: string) {
  return admin.blockKey({
    productId: PRODUCT_ID,
    licenseKey,
  });
}

export async function unblockLicense(licenseKey: string) {
  return admin.unblockKey({
    productId: PRODUCT_ID,
    licenseKey,
  });
}

// Requires an admin or read-only key. The details are in data.license.
export async function getLicenseInfo(licenseKey: string) {
  const result = await admin.getKey({ productId: PRODUCT_ID, licenseKey });
  return result.data.license;
}

export async function updateLicenseExpiry(
  licenseKey: string,
  expiryDate: Date,
) {
  return admin.updateKey({
    productId: PRODUCT_ID,
    licenseKey,
    expiryDate: expiryDate.toISOString(),
  });
}

Wire it into a Next.js API route for your dashboard:

typescript
import { NextRequest, NextResponse } from 'next/server';
import { createCustomerLicense } from '@/lib/license-service';
import { getServerSession } from '@/lib/auth';

export async function POST(req: NextRequest) {
  const session = await getServerSession();
  if (!session) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  const body = await req.json();
  const { customerEmail, customerName, maxActivations, planName } = body;

  try {
    const license = await createCustomerLicense({
      customerEmail,
      customerName,
      maxActivations,
      planName,
    });

    return NextResponse.json({
      success: true,
      licenseKey: license.key,
    });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create license' },
      { status: 500 },
    );
  }
}

Validate License

Your customers run self-hosted instances that need to validate their license. Provide them with a validation endpoint they call from their instance.

The SDK throws on every non-2xx response, with status, code and errorCode on the error. A 401, 402, 403 or 404 (product deactivated or removed) means Keymint rejected the license; anything else is a temporary failure that the instance should retry rather than treat as "invalid":

typescript
// This route is called BY your customer's self-hosted instance
// It proxies the activation to Keymint
import { NextRequest, NextResponse } from 'next/server';
import { KeyMint } from 'keymint';

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

export async function POST(req: NextRequest) {
  const body = await req.json();
  const { licenseKey, hostId } = body;

  if (!licenseKey || !hostId) {
    return NextResponse.json(
      { error: 'licenseKey and hostId are required' },
      { status: 400 },
    );
  }

  try {
    const result = await client.activateKey({
      productId: PRODUCT_ID,
      licenseKey,
      hostId,
    });
    return NextResponse.json({ valid: true, message: result.message });
  } catch (error: any) {
    if ([401, 402, 403, 404].includes(error?.status)) {
      return NextResponse.json({ valid: false, errorCode: error.errorCode });
    }
    // Keymint unreachable, rate limited or erroring: tell the instance to retry
    return NextResponse.json(
      { error: 'License service temporarily unavailable' },
      { status: 503 },
    );
  }
}

Or, have your customers call Keymint directly from their self-hosted instance using their own client API key. You can create product-scoped API keys for each customer from the Keymint Dashboard:

  1. Navigate to Developer → API Keys
  2. Create a key with client scope, scoped to your product
  3. Share this key with your customer for their instance

The customer's instance then calls Keymint directly:

typescript
// In your customer's self-hosted code
import { KeyMint } from 'keymint';

const client = new KeyMint('CUSTOMER_CLIENT_API_KEY');

try {
  await client.activateKey({
    productId: 'YOUR_PRODUCT_ID',
    licenseKey: 'A8E2K-9F1BC-3D4GH-7J2KM',
    hostId: KeyMint.getOrCreateInstallationId(),
  });
  console.log('License valid');
} catch (error: any) {
  if ([401, 402, 403, 404].includes(error?.status)) {
    console.error('License rejected:', error.errorCode);
  } else {
    console.error('Could not reach Keymint; retry later');
  }
}

Handle Activations

Display license usage in your Next.js dashboard using Server Components. Look keys up with an admin or read-only key, using the raw keys you stored at creation: the API finds a license only by its key. data.license.key is the raw key for admin keys and masked for read-only keys:

typescript
import { KeyMint } from 'keymint';

const admin = new KeyMint(process.env.KEYMINT_ADMIN_API_KEY!);
const PRODUCT_ID = process.env.KEYMINT_PRODUCT_ID!;

async function getLicenseData(licenseKeys: string[]) {
  const results = await Promise.all(
    licenseKeys.map(async (key) => {
      try {
        const { data } = await admin.getKey({ productId: PRODUCT_ID, licenseKey: key });
        return { key, license: data.license };
      } catch {
        return null;
      }
    }),
  );
  return results.filter((r) => r !== null);
}

export default async function LicensesPage() {
  // In production, fetch license keys from your database
  const customerLicenseKeys = await db.license.findMany({
    where: { userId: session.user.id },
    select: { key: true },
  });

  const licenses = await getLicenseData(
    customerLicenseKeys.map((l) => l.key),
  );

  return (
    <div>
      <h1>Your Licenses</h1>
      <table>
        <thead>
          <tr>
            <th>License Key</th>
            <th>Activations</th>
            <th>Status</th>
            <th>Expires</th>
          </tr>
        </thead>
        <tbody>
          {licenses.map(({ key, license }) => (
            <tr key={key}>
              <td>{key.substring(0, 12)}...</td>
              <td>
                {license.activations}/{license.maxActivations ?? 'unlimited'}
              </td>
              <td>
                {license.activated ? 'Active' : 'Inactive'}
              </td>
              <td>
                {license.expirationDate
                  ? new Date(license.expirationDate).toLocaleDateString()
                  : 'Never'}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Handle Revocations

When a Stripe subscription cancels, block the license. Wire this into your Stripe webhook handler:

typescript
import { NextRequest } from "next/server";
import Stripe from "stripe";
import { blockLicense } from "@/lib/license-service";

const stripe = new Stripe(process.env.STRIPE_SECRET_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 === "customer.subscription.deleted") {
    const subscription = event.data.object as Stripe.Subscription;
    const customerId = subscription.customer as string;

    // Find the license key linked to this Stripe customer
    const licenseKeys = await getLicenseKeysForStripeCustomer(customerId);

    for (const key of licenseKeys) {
      await blockLicense(key);
      console.log(`Blocked license ${key} for cancelled subscription`);
    }
  }

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

You can also expose a manual revoke action in your dashboard:

typescript
import { NextRequest, NextResponse } from 'next/server';
import { blockLicense } from '@/lib/license-service';

export async function POST(
  req: NextRequest,
  { params }: { params: Promise<{ key: string }> },
) {
  const { key } = await params; // Next.js 15: params is a Promise
  try {
    await blockLicense(key);
    return NextResponse.json({ success: true });
  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to block license' },
      { status: 500 },
    );
  }
}

Production Considerations

Environment Variables

text
KEYMINT_ADMIN_API_KEY=km_live_...   # admin-scoped key
KEYMINT_CLIENT_API_KEY=km_live_...  # client-scoped key
KEYMINT_PRODUCT_ID=YOUR_PRODUCT_ID
KEYMINT_WEBHOOK_SECRET=...          # signing secret of your Keymint webhook endpoint
  • KEYMINT_ADMIN_API_KEY — used server-side only (license creation, blocking, info lookup)
  • KEYMINT_CLIENT_API_KEY — used for activation proxying or shared with customers
  • KEYMINT_PRODUCT_ID — the product all licenses belong to

Keymint Webhooks for Your SaaS

Register Keymint outbound webhooks to keep your database in sync:

Keymint EventWhat to do
license.createdStore license in your database
device.activatedLog activation, update seat count in your UI
device.deactivatedFree up a seat in your UI
license.disabledNotify customer, update status
license.enabledReactivate after payment

Configure webhook endpoints under Developer → Webhooks in the Keymint Dashboard.

Verify the Keymint-Signature header against the raw body before you trust the event. The SDK's KeyMint.verifyWebhookSignature() checks the HMAC-SHA256 signature over timestamp + "." + raw_body and rejects timestamps more than 300 seconds old:

typescript
import { NextRequest } from 'next/server';
import { KeyMint } from 'keymint';

export async function POST(req: NextRequest) {
  const payload = await req.text();
  const signature = req.headers.get('keymint-signature') ?? '';

  if (!KeyMint.verifyWebhookSignature(payload, signature, process.env.KEYMINT_WEBHOOK_SECRET!)) {
    return Response.json({ error: 'Invalid signature' }, { status: 400 });
  }

  const event = JSON.parse(payload);
  console.log(`Received webhook: ${event.type}`);

  // Handle based on event.type
  switch (event.type) {
    case 'device.activated':
      // Update your activation count
      break;
    case 'license.disabled':
      // Notify customer
      break;
  }

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

Stripe Integration at Scale

For production, store the link between Stripe customer IDs and Keymint license keys in your own database:

text
stripe_customer_id | license_key       | plan    | status
cus_abc123         | A8E2K-9F1BC-...   | pro     | active
cus_def456         | T8XTR-AI21B-...   | starter | expired

This lets your Stripe webhook handler find and block/unblock the right license key when subscriptions change.

Rate Limits

PlanRequests/min
Free30
Maker50
Startup100
Standard1,000

For SaaS usage, Standard is recommended. The activation endpoint (POST /key/activate) should be called once per instance startup — not on every request.

Floating Licenses for Higher Tiers

If your customers need concurrent seat licensing (e.g. 50 seats shared across a team), use floating licenses:

typescript
// Always set maxConcurrentSessions: leaving it empty means unlimited seats.
const license = await admin.createKey({
  productId: PRODUCT_ID,
  licenseType: 'floating',
  maxConcurrentSessions: 50,
  heartbeatInterval: 120,
  sessionLeaseDuration: 300,
  newCustomer: { name: 'Acme Corp', email: 'it@acme.com' },
});

Customers then use POST /key/checkout, POST /key/heartbeat, and POST /key/checkin to manage seats. See Floating Licenses.

Complete Source Code

Full license service (src/lib/license-service.ts)
typescript
import { KeyMint } from 'keymint';
import { db } from '@/lib/db';
import { licenseKeys } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';

const admin = new KeyMint(process.env.KEYMINT_ADMIN_API_KEY!);
const PRODUCT_ID = process.env.KEYMINT_PRODUCT_ID!;

interface CreateLicenseParams {
  customerEmail: string;
  customerName: string;
  maxActivations: number;
  planName: string;
  subscriptionEndDate?: Date;
  userId: string;
}

export async function createCustomerLicense(params: CreateLicenseParams) {
  const license = await admin.createKey({
    productId: PRODUCT_ID,
    maxActivations: String(params.maxActivations),
    expiryDate: params.subscriptionEndDate?.toISOString(),
    newCustomer: {
      name: params.customerName,
      email: params.customerEmail,
    },
    metadata: {
      plan: params.planName,
    },
  });

  // Store in your database
  await db.insert(licenseKeys).values({
    userId: params.userId,
    licenseKey: license.key,
    productId: PRODUCT_ID,
    status: 'active',
    plan: params.planName,
  });

  return license;
}

export async function blockLicense(licenseKey: string) {
  await admin.blockKey({ productId: PRODUCT_ID, licenseKey });
  await db
    .update(licenseKeys)
    .set({ status: 'blocked' })
    .where(eq(licenseKeys.licenseKey, licenseKey));
}

export async function unblockLicense(licenseKey: string) {
  await admin.unblockKey({ productId: PRODUCT_ID, licenseKey });
  await db
    .update(licenseKeys)
    .set({ status: 'active' })
    .where(eq(licenseKeys.licenseKey, licenseKey));
}

export async function getLicenseInfo(licenseKey: string) {
  const result = await admin.getKey({ productId: PRODUCT_ID, licenseKey });
  return result.data.license;
}

export async function updateLicenseExpiry(
  licenseKey: string,
  expiryDate: Date,
) {
  return admin.updateKey({
    productId: PRODUCT_ID,
    licenseKey,
    expiryDate: expiryDate.toISOString(),
  });
}

export async function getCustomerLicenses(userId: string) {
  return db
    .select()
    .from(licenseKeys)
    .where(eq(licenseKeys.userId, userId));
}

On this page

  • Problem
  • Architecture Diagram
  • Create Product
  • Create License Policy
  • Issue License
  • Validate License
  • Handle Activations
  • Handle Revocations
  • Production Considerations
  • Environment Variables
  • Keymint Webhooks for Your SaaS
  • Stripe Integration at Scale
  • Rate Limits
  • Floating Licenses for Higher Tiers
  • Complete Source Code