Shipped
HomeContacts
  • Get started
  • 📚Tutorials
    • Make a waiting list
    • Launch a pre-sale
    • Build a SaaS
    • Create your store on Lemon Squeezy
  • 🟩Features
    • AI Services
    • Affiliate Program
    • Analytics
    • Authentication
      • MailChimp
      • Loops
      • AWS SES
      • SendGrid
      • Supabase Auth
        • Supabase Authentication Flow
        • Supabase Magic Link
        • Supabase Email & Password
        • Supabase Login with Google
    • API endpoints
      • 🛡️Authenticated API
    • Blog
    • Customer support
    • Chrome Extension
    • Dark mode
    • Database
      • Update your database
      • MongoDB
    • Emails
    • Error pages
    • Icons
    • Onboarding
    • Payments
      • Lemon Squeezy
        • Subscriptions
        • One-time purchase
        • Test mode
      • Stripe
    • Private pages
    • SEO
    • shadcn/ui
    • Supabase
    • Workspace / Organizations
  • 📦Components
    • AccountMenu
    • CtaBox
    • DarkModeSwitch
    • Explainer video
    • FAQ
    • Features
    • Footer
    • Header
    • Hero
    • Lifetime
    • Pricing
    • Sales Notification
    • Secondary Sidebar Pages
    • Sidebar
    • Tabs
    • Testimonials
    • Waitlist
    • WebAppPage
  • 🚀Deployment
  • ✅Other
    • Configuration
    • Changelog widget
    • Favicon
    • Google Fonts
    • Sitemap
    • Theme
  • Updates
  • GitHub Repository
  • Support
Powered by GitBook
On this page
  • Database
  • Lemon Squeezy

Was this helpful?

  1. Features
  2. Payments
  3. Lemon Squeezy

One-time purchase

Manage the orders of your product.

When a new purchase occurs, the code stores the event in a table called Order.

Here are the steps needed to support it.

Database

Add this table definition to your prisma/schema.prisma file:

schema.prisma
model Order {
  id                  Int       @id @default(autoincrement())
  email               String
  lemonOrderId        String?
  lemonProductId      String
  lemonVariantId      String?
  lemonVariantName    String?
  lemonPlanName       String?
  lemonPlanPrice      String?
  lemonSubscriptionId String?
  createdAt           DateTime  @default(now())
  updatedAt           DateTime  @updatedAt
  validUntil          DateTime?
  cancelUrl           String?
  updateUrl           String?
  status              String?
}

Push the new table to your database:

terminal
npx prisma db push

Then open the file src/app/api/webhooks/lemonsqueezy/route.ts and replace its content with this:

src/app/api/webhooks/lemonsqueezy/route.ts
import { NextResponse } from "next/server";
import rawBody from "raw-body";
import crypto from "crypto";
import { Readable } from "stream";
import { headers } from "next/headers";
import { prismaClient } from "@/prisma/db";

type ISODate = string;

export type WebhookRequest = {
  meta: {
    event_name:
      | "order_created"
      | "order_refunded"
      | "subscription_created"
      | "subscription_updated"
      | "subscription_cancelled"
      | "subscription_resumed"
      | "subscription_expired"
      | "subscription_paused"
      | "subscription_unpaused"
      | "subscription_payment_failed"
      | "subscription_payment_success"
      | "subscription_payment_recovered"
      | "license_key_created"
      | "license_key_updated";
  };
  data: {
    type: "subscriptions";
    id: string;
    attributes: {
      store_id: number;
      customer_id: number;
      order_id: number;
      order_item_id: number;
      product_id: number;
      variant_id: number;
      product_name: string;
      variant_name: string;
      user_name: string;
      user_email: string;
      status: string;
      status_formatted: string;
      card_brand: string;
      card_last_four: string;
      pause: null;
      cancelled: boolean;
      trial_ends_at: ISODate;
      billing_anchor: number;
      urls: {
        update_payment_method: string;
      };
      renews_at: "2023-01-24T12:43:48.000000Z";
      ends_at: null;
      created_at: "2023-01-17T12:43:50.000000Z";
      updated_at: "2023-01-17T12:43:51.000000Z";
      test_mode: false;
      first_order_item: {
        id: number;
        price: number;
        order_id: number;
        price_id: number;
        test_mode: boolean;
        created_at: ISODate;
        product_id: number;
        updated_at: ISODate;
        variant_id: number;
        product_name: string;
        variant_name: string;
      };
    };
    relationships: {
      store: {
        links: {
          related: string;
          self: string;
        };
      };
      customer: {
        links: {
          related: string;
          self: string;
        };
      };
      order: {
        links: {
          related: string;
          self: string;
        };
      };
      "order-item": {
        links: {
          related: string;
          self: string;
        };
      };
      product: {
        links: {
          related: string;
          self: string;
        };
      };
      variant: {
        links: {
          related: string;
          self: string;
        };
      };
      "subscription-invoices": {
        links: {
          related: string;
          self: string;
        };
      };
    };
    links: {
      self: string;
    };
  };
};

export async function POST(request: Request) {
  const body = await rawBody(Readable.from(Buffer.from(await request.text())));
  const headersList = headers();
  const payload: WebhookRequest = JSON.parse(body.toString());
  console.log(">>> payload", payload);
  const sigString = headersList.get("x-signature");
  const secret = process.env.LEMONSQUEEZY_WEBHOOK_SECRET as string;
  const hmac = crypto.createHmac("sha256", secret);
  const digest = Buffer.from(hmac.update(body).digest("hex"), "utf8");
  const signature = Buffer.from(
    Array.isArray(sigString) ? sigString.join("") : sigString || "",
    "utf8"
  );

  // validate signature
  if (!crypto.timingSafeEqual(digest, signature)) {
    return NextResponse.json({ message: "Invalid signature" }, { status: 403 });
  }

  const userEmail = payload.data.attributes.user_email;

  const eventName = payload.meta.event_name;

  const userOrder = await prismaClient.order.findFirst({
    where: {
      email: userEmail,
      status: "CREATED",
    },
  });

  if (eventName === "order_created") {
    await prismaClient.order.create({
      data: {
        email: userEmail,
        lemonOrderId:
          payload.data.attributes.first_order_item.order_id.toString(),
        lemonProductId:
          payload.data.attributes.first_order_item.product_id.toString(),
        lemonVariantId:
          payload.data.attributes.first_order_item.variant_id.toString(),
        lemonVariantName: payload.data.attributes.first_order_item.variant_name,
        lemonPlanName: payload.data.attributes.first_order_item.product_name,
        lemonPlanPrice: null,
        lemonSubscriptionId: null,
        validUntil: new Date(),
        updateUrl: null,
        status: "CREATED",
      },
    });

  }

  if (eventName === "order_refunded") {
    if (userOrder) {
      await prismaClient.order.update({
        where: {
          id: userOrder.id,
        },
        data: {
          status: "REFUNDED",
        },
      });
    }

    
  }

  return NextResponse.json({ result: true }, { status: 200 });
}

Lemon Squeezy

Enable the order_created order_refunded events on Lemon Squeezy, in the Webhook configuration.

Settings > Webhooks > Edit the WebHook

Now, whenever a customer buys your product, a new record will be created in the table public.Order

PreviousSubscriptionsNextTest mode

Last updated 1 year ago

Was this helpful?

🟩