> ## 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.

# Custom Stripe webhooks

> Host your own Stripe webhook and call the Developer API when the managed Commerce pipeline is not enough.

The supported way to sell licenses through Stripe is **[Commerce](/features/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](/api/overview#authentication)
* A Stripe account with API keys
* A Node.js backend (examples use Express.js)

## One-time purchases

### Flow overview

```mermaid theme={null}
sequenceDiagram
    participant Customer
    participant YourSite as Your Website
    participant Stripe
    participant YourServer as Your Server
    participant AF as AuthForge API

    Customer->>YourSite: Click "Buy License"
    YourSite->>Stripe: Create Checkout Session
    Stripe-->>Customer: Redirect to Checkout
    Customer->>Stripe: Complete payment
    Stripe->>YourServer: Webhook: checkout.session.completed
    YourServer->>AF: POST /v1/licenses
    AF-->>YourServer: License key
    YourServer->>Customer: Deliver key (email / success page)
```

### 1. Create a Stripe Checkout session

```javascript theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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.

```javascript theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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

```javascript theme={null}
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:

1. Storing the Stripe session/subscription ID alongside each license in your database.
2. Checking for an existing record before creating a new license.
3. Returning early if a license already exists for that payment.

## Next steps

* [Commerce](/features/commerce); Managed Stripe setup (start here unless you need this guide)
* [Subscription Lifecycle Management](/guides/subscriptions); Detailed subscription patterns
* [Custom Integration Guide](/guides/custom-integration); Build your own payment integration
