Skip to main content
This guide covers patterns for managing licenses tied to recurring subscriptions, whether you use Stripe, Paddle, or any other payment provider.

Lifecycle overview

Creating a subscription license

When a subscription starts, create a license with an expiration matching the billing period:
const response = await fetch("https://api.authforge.cc/v1/licenses", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    appId: process.env.AUTHFORGE_APP_ID,
    quantity: 1,
    expiresAt: periodEndDate.toISOString(),
    maxHwidSlots: 1,
    label: `sub_${subscriptionId}`,
  }),
});
Store the mapping between your subscription ID and the AuthForge license key in your database.

Renewal

When a subscription renews successfully, extend the license expiration:
await fetch(`https://api.authforge.cc/v1/licenses/${licenseKey}`, {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    action: "extend",
    expiresAt: newPeriodEndDate.toISOString(),
  }),
});
The user’s SDK session continues uninterrupted — the next heartbeat or re-authentication will pick up the new expiry.

Cancellation

When a user cancels, you have two options:

Option A: Immediate revocation

Revoke the license immediately on cancellation:
await fetch(`https://api.authforge.cc/v1/licenses/${licenseKey}`, {
  method: "PUT",
  headers: {
    Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ action: "revoke" }),
});

Option B: Let it expire

Don’t revoke — let the license expire at the end of the already-paid period. This is the more user-friendly approach. The license expiresAt was set to the billing period end, so it will naturally stop working.
Option B is the standard practice. Users have paid for the current period and should retain access until it ends.

Payment failure

When a payment fails, decide how to handle the grace period:
async function handlePaymentFailed(subscriptionId, attemptCount) {
  const record = await db.getLicenseBySubscription(subscriptionId);
  if (!record) return;

  if (attemptCount >= 3) {
    // Final attempt failed — revoke
    await revokeLicense(record.licenseKey);
    await notifyCustomer(record.email, "subscription_cancelled");
  } else {
    // Early attempt — warn the user but keep the license active
    await notifyCustomer(record.email, "payment_failed");
  }
}

Re-subscription

When a user re-subscribes after cancellation or revocation:
  1. Check if they have an existing (revoked) license key.
  2. If yes, re-activate it and extend the expiry — this preserves their HWID bindings.
  3. If no, create a new license.
async function handleResubscription(customerId, newPeriodEnd) {
  const existing = await db.getLicenseByCustomer(customerId);

  if (existing) {
    // Re-activate the existing license
    await fetch(`https://api.authforge.cc/v1/licenses/${existing.licenseKey}`, {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ action: "activate" }),
    });

    // Extend expiry
    await fetch(`https://api.authforge.cc/v1/licenses/${existing.licenseKey}`, {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        action: "extend",
        expiresAt: newPeriodEnd.toISOString(),
      }),
    });
  } else {
    // Create a fresh license
    await createNewLicense(customerId, newPeriodEnd);
  }
}

Plan upgrades and downgrades

Use license variables to control features per plan tier. When a user upgrades or downgrades:
async function handlePlanChange(licenseKey, newPlan) {
  const planConfig = {
    basic: { plan: "basic", maxProjects: 3 },
    pro: { plan: "pro", maxProjects: 10 },
    enterprise: { plan: "enterprise", maxProjects: 50 },
  };

  await fetch(
    `https://api.authforge.cc/v1/licenses/${licenseKey}/variables`,
    {
      method: "PUT",
      headers: {
        Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify(planConfig[newPlan]),
    }
  );
}
The user picks up the new plan features the next time they authenticate.

Database schema suggestion

Store the mapping between your payment provider and AuthForge:
CREATE TABLE subscription_licenses (
  id SERIAL PRIMARY KEY,
  customer_id TEXT NOT NULL,
  customer_email TEXT NOT NULL,
  subscription_id TEXT UNIQUE NOT NULL,
  license_key TEXT UNIQUE NOT NULL,
  plan TEXT NOT NULL DEFAULT 'basic',
  status TEXT NOT NULL DEFAULT 'active',
  created_at TIMESTAMP DEFAULT NOW(),
  updated_at TIMESTAMP DEFAULT NOW()
);

Next steps