Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.authforge.cc/llms.txt

Use this file to discover all available pages before exploring further.

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