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.
The supported way to sell licenses through Stripe is Commerce: paste keys in the dashboard, map prices to license templates, and AuthForge verifies webhooks, deduplicates events, issues keys, and can email buyers for you.
This guide is the fallback: integrate Stripe yourself by receiving webhooks on your server and calling POST /v1/licenses (and related endpoints). Reach for it when you need full control over Checkout sessions (custom line items, bundled purchases, bespoke metadata), or a flow Commerce does not cover yet.
Prerequisites
- An AuthForge account with an app created
- An AuthForge Developer API key
- A Stripe account with API keys
- A Node.js backend (examples use Express.js)
One-time purchases
Flow overview
1. Create a Stripe Checkout session
const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY);
app.post("/api/create-checkout", async (req, res) => {
const session = await stripe.checkout.sessions.create({
mode: "payment",
payment_method_types: ["card"],
line_items: [
{
price: "price_your_product_price_id",
quantity: 1,
},
],
success_url: "https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "https://yoursite.com/pricing",
metadata: {
authforge_app_id: process.env.AUTHFORGE_APP_ID,
license_duration: "365", // days, or omit for lifetime
max_hwid_slots: "1",
},
});
res.json({ url: session.url });
});
2. Handle the Stripe webhook
const crypto = require("crypto");
app.post(
"/webhooks/stripe",
express.raw({ type: "application/json" }),
async (req, res) => {
const sig = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
console.error("Webhook signature verification failed:", err.message);
return res.sendStatus(400);
}
if (event.type === "checkout.session.completed") {
const session = event.data.object;
await handleCompletedCheckout(session);
}
res.sendStatus(200);
}
);
3. Generate the license key
async function handleCompletedCheckout(session) {
const { authforge_app_id, license_duration, max_hwid_slots } =
session.metadata;
// Idempotency: check if we've already created a license for this session
const existingLicense = await db.getLicenseByStripeSession(session.id);
if (existingLicense) {
console.log("License already created for session", session.id);
return;
}
// Calculate expiration
const expiresAt = license_duration
? new Date(Date.now() + parseInt(license_duration) * 86400000).toISOString()
: null;
// Create license via AuthForge API
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: authforge_app_id,
quantity: 1,
expiresAt,
maxHwidSlots: parseInt(max_hwid_slots) || 1,
label: `Stripe session ${session.id}`,
}),
});
const data = await response.json();
const licenseKey = data.licenses[0].licenseKey;
// Store the mapping in your database
await db.saveLicense({
stripeSessionId: session.id,
customerEmail: session.customer_details.email,
licenseKey,
});
// Deliver the key to the customer
await sendLicenseEmail(session.customer_details.email, licenseKey);
}
4. Deliver the key
Options for delivering the license key to the customer:
- Email: Send an automated email with the key after payment.
- Success page: Redirect to a page that displays the key (retrieve it by session ID).
- Customer portal: Store keys in your database and let users view them in their account.
app.get("/success", async (req, res) => {
const { session_id } = req.query;
const license = await db.getLicenseByStripeSession(session_id);
if (!license) {
return res.status(404).send("License not found. Please check your email.");
}
res.render("success", { licenseKey: license.licenseKey });
});
Subscriptions
For recurring payments, you’ll generate a license when the subscription starts and revoke it when the subscription ends.
Events to handle
| Stripe Event | AuthForge Action |
|---|
customer.subscription.created | Create a license key |
customer.subscription.deleted | Revoke the license key |
customer.subscription.updated | Extend expiry on renewal |
invoice.payment_failed | Optionally revoke or warn |
Complete subscription handler
app.post(
"/webhooks/stripe",
express.raw({ type: "application/json" }),
async (req, res) => {
const sig = req.headers["stripe-signature"];
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
} catch (err) {
return res.sendStatus(400);
}
switch (event.type) {
case "customer.subscription.created": {
const subscription = event.data.object;
await handleSubscriptionCreated(subscription);
break;
}
case "customer.subscription.deleted": {
const subscription = event.data.object;
await handleSubscriptionDeleted(subscription);
break;
}
case "customer.subscription.updated": {
const subscription = event.data.object;
await handleSubscriptionUpdated(subscription);
break;
}
case "invoice.payment_failed": {
const invoice = event.data.object;
await handlePaymentFailed(invoice);
break;
}
}
res.sendStatus(200);
}
);
Subscription created; generate license
async function handleSubscriptionCreated(subscription) {
// Guard against duplicate processing (Stripe retries webhooks)
const existing = await db.getLicenseBySubscription(subscription.id);
if (existing) return;
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: new Date(subscription.current_period_end * 1000).toISOString(),
maxHwidSlots: 1,
label: `Stripe sub ${subscription.id}`,
}),
});
const data = await response.json();
const licenseKey = data.licenses[0].licenseKey;
await db.saveSubscriptionLicense({
stripeSubscriptionId: subscription.id,
customerId: subscription.customer,
licenseKey,
});
const customer = await stripe.customers.retrieve(subscription.customer);
await sendLicenseEmail(customer.email, licenseKey);
}
Subscription deleted; revoke license
async function handleSubscriptionDeleted(subscription) {
const record = await db.getLicenseBySubscription(subscription.id);
if (!record) return;
await fetch(
`https://api.authforge.cc/v1/licenses/${record.licenseKey}`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ action: "revoke" }),
}
);
}
Subscription updated; extend expiry on renewal
async function handleSubscriptionUpdated(subscription) {
if (subscription.status !== "active") return;
const record = await db.getLicenseBySubscription(subscription.id);
if (!record) return;
await fetch(
`https://api.authforge.cc/v1/licenses/${record.licenseKey}`,
{
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "extend",
expiresAt: new Date(subscription.current_period_end * 1000).toISOString(),
}),
}
);
}
Payment failed; optional revocation
async function handlePaymentFailed(invoice) {
const subscriptionId = invoice.subscription;
if (!subscriptionId) return;
const record = await db.getLicenseBySubscription(subscriptionId);
if (!record) return;
// Option 1: Revoke immediately
// await revokeLicense(record.licenseKey);
// Option 2: Send a warning email, revoke after grace period
const customer = await stripe.customers.retrieve(invoice.customer);
await sendPaymentFailedEmail(customer.email);
}
Idempotency
Stripe retries webhook deliveries on failure. Guard against duplicate license creation by:
- Storing the Stripe session/subscription ID alongside each license in your database.
- Checking for an existing record before creating a new license.
- Returning early if a license already exists for that payment.
Next steps