Back to roadmaps lemonsqueezy Course

Processing Order and Subscription Webhook Events

Once a webhook payload is verified as authentic, parse the event action type to register orders, link software licenses, or block past-due subscriptions.


1. Key Webhook Event Action Names

Monitor these events to synchronize status values inside your database:

  • order_created: Fired when a customer completes a one-time purchase or initial subscription purchase. Includes billing details and order totals.
  • subscription_created: Fired when a subscription is registered. Contains the status (active or trialing).
  • subscription_payment_failed: Fired when recurring invoice payments fail. Use this to notify the customer and block premium access.

2. Implementing the Event Handler Engine

Extend your route handler function to update Postgres/MySQL states:

// app/api/webhooks/lemonsqueezy/route.ts (Continued)
import { prisma } from "../../../lib/prisma";

export async function processLemonEvent(payload: any) {
  const eventName = payload.meta.event_name;
  const data = payload.data;
  const attributes = data.attributes;

  switch (eventName) {
    case "order_created": {
      const customerId = attributes.customer_id;
      const orderNumber = attributes.order_number;
      const totalAmount = attributes.total_formatted;

      // Extract custom user ID from transaction pass-through data
      const userId = payload.meta.custom_data?.userId;

      if (userId) {
        await prisma.order.create({
          data: {
            userId: userId,
            lemonCustomerId: String(customerId),
            orderNumber: String(orderNumber),
            amount: totalAmount,
            status: "paid",
          },
        });
      }
      break;
    }

    case "subscription_created": {
      const subscriptionId = data.id;
      const customerId = attributes.customer_id;
      const status = attributes.status; // e.g. "active"
      
      const userId = payload.meta.custom_data?.userId;

      if (userId) {
        await prisma.user.update({
          where: { id: userId },
          data: {
            lemonCustomerId: String(customerId),
            lemonSubscriptionId: String(subscriptionId),
            subscriptionActive: status === "active" || status === "on_trial",
          },
        });
      }
      break;
    }

    case "subscription_payment_failed": {
      const subscriptionId = data.id;

      // Restrict access until payment details are updated
      await prisma.user.updateMany({
        where: { lemonSubscriptionId: String(subscriptionId) },
        data: {
          subscriptionActive: false,
        },
      });
      break;
    }

    default:
      console.log(`Ignored event action: ${eventName}`);
  }
}

3. Important Design Advice

  • Atomic Transactions: If an order creates multiple side-effects (such as generating a license key and adding a customer profile row), use a Prisma $transaction database helper block to make sure either all writes succeed or all roll back.
  • Return 200 OK Fast: Webhook servers expect your server to return an HTTP status code 200 immediately. Do not execute slow, complex tasks (like processing video files or sending long emails) synchronously inside the webhook call; trigger background queues instead.
Published on Last updated: