Skip to content

Instantly share code, notes, and snippets.

@amosbastian
Last active August 6, 2024 20:08

Revisions

  1. amosbastian revised this gist May 22, 2023. 1 changed file with 11 additions and 11 deletions.
    22 changes: 11 additions & 11 deletions route.ts
    Original file line number Diff line number Diff line change
    @@ -5,6 +5,10 @@ import { NextRequest } from "next/server";
    // Put this in your billing lib and just import the type instead
    type LemonsqueezySubscription = Awaited<ReturnType<typeof listAllSubscriptions>>["data"][number];

    const isError = (error: unknown): error is Error => {
    return error instanceof Error;
    };

    // Add more events here if you want
    // https://docs.lemonsqueezy.com/api/webhooks#event-types
    type EventName =
    @@ -29,12 +33,12 @@ type Payload = {
    data: LemonsqueezySubscription;
    };

    export const POST = async (req: NextRequest) => {
    export const POST = async (request: NextRequest) => {
    try {
    const text = await req.text();
    const text = await request.text();
    const hmac = crypto.createHmac("sha256", process.env["LEMON_SQUEEZY_WEBHOOK_SECRET"] || "");
    const digest = Buffer.from(hmac.update(text).digest("hex"), "utf8");
    const signature = Buffer.from(req.headers.get("x-signature") as string, "utf8");
    const signature = Buffer.from(request.headers.get("x-signature") as string, "utf8");

    if (!crypto.timingSafeEqual(digest, signature)) {
    return new Response("Invalid signature.", {
    @@ -72,19 +76,15 @@ export const POST = async (req: NextRequest) => {
    throw new Error(`🤷‍♀️ Unhandled event: ${eventName}`);
    }
    } catch (error: unknown) {
    if (typeof error === "string") {
    return new Response("Webhook error", {
    status: 400,
    });
    }

    if (error instanceof Error) {
    if (isError(error)) {
    return new Response(`Webhook error: ${error.message}`, {
    status: 400,
    });
    }

    throw error;
    return new Response("Webhook error", {
    status: 400,
    });
    }

    return new Response(null, {
  2. amosbastian revised this gist May 21, 2023. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions route.ts
    Original file line number Diff line number Diff line change
    @@ -31,9 +31,9 @@ type Payload = {

    export const POST = async (req: NextRequest) => {
    try {
    const rawBody = await req.text();
    const text = await req.text();
    const hmac = crypto.createHmac("sha256", process.env["LEMON_SQUEEZY_WEBHOOK_SECRET"] || "");
    const digest = Buffer.from(hmac.update(rawBody).digest("hex"), "utf8");
    const digest = Buffer.from(hmac.update(text).digest("hex"), "utf8");
    const signature = Buffer.from(req.headers.get("x-signature") as string, "utf8");

    if (!crypto.timingSafeEqual(digest, signature)) {
    @@ -42,7 +42,7 @@ export const POST = async (req: NextRequest) => {
    });
    }

    const payload = JSON.parse(rawBody);
    const payload = JSON.parse(text);

    const {
    meta: { event_name: eventName },
  3. amosbastian revised this gist May 21, 2023. No changes.
  4. amosbastian revised this gist May 21, 2023. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion route.ts
    Original file line number Diff line number Diff line change
    @@ -25,13 +25,14 @@ type Payload = {
    test_mode: boolean;
    event_name: EventName;
    };
    // Possibly not accurate: it's missing the relationships field and any custom data you add
    data: LemonsqueezySubscription;
    };

    export const POST = async (req: NextRequest) => {
    try {
    const rawBody = await req.text();
    const hmac = crypto.createHmac("sha256", process.env.LEMON_SQUEEZY_WEBHOOK_SECRET || "");
    const hmac = crypto.createHmac("sha256", process.env["LEMON_SQUEEZY_WEBHOOK_SECRET"] || "");
    const digest = Buffer.from(hmac.update(rawBody).digest("hex"), "utf8");
    const signature = Buffer.from(req.headers.get("x-signature") as string, "utf8");

  5. amosbastian created this gist May 21, 2023.
    92 changes: 92 additions & 0 deletions route.ts
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,92 @@
    import crypto from "crypto";
    import { listAllSubscriptions } from "lemonsqueezy.ts";
    import { NextRequest } from "next/server";

    // Put this in your billing lib and just import the type instead
    type LemonsqueezySubscription = Awaited<ReturnType<typeof listAllSubscriptions>>["data"][number];

    // Add more events here if you want
    // https://docs.lemonsqueezy.com/api/webhooks#event-types
    type EventName =
    | "order_created"
    | "order_refunded"
    | "subscription_created"
    | "subscription_cancelled"
    | "subscription_resumed"
    | "subscription_expired"
    | "subscription_paused"
    | "subscription_unpaused"
    | "subscription_payment_failed"
    | "subscription_payment_success"
    | "subscription_payment_recovered";

    type Payload = {
    meta: {
    test_mode: boolean;
    event_name: EventName;
    };
    data: LemonsqueezySubscription;
    };

    export const POST = async (req: NextRequest) => {
    try {
    const rawBody = await req.text();
    const hmac = crypto.createHmac("sha256", process.env.LEMON_SQUEEZY_WEBHOOK_SECRET || "");
    const digest = Buffer.from(hmac.update(rawBody).digest("hex"), "utf8");
    const signature = Buffer.from(req.headers.get("x-signature") as string, "utf8");

    if (!crypto.timingSafeEqual(digest, signature)) {
    return new Response("Invalid signature.", {
    status: 400,
    });
    }

    const payload = JSON.parse(rawBody);

    const {
    meta: { event_name: eventName },
    data: subscription,
    } = payload as Payload;

    switch (eventName) {
    case "order_created":
    // Do stuff here if you are using orders
    break;
    case "order_refunded":
    // Do stuff here if you are using orders
    break;
    case "subscription_created":
    case "subscription_cancelled":
    case "subscription_resumed":
    case "subscription_expired":
    case "subscription_paused":
    case "subscription_unpaused":
    case "subscription_payment_failed":
    case "subscription_payment_success":
    case "subscription_payment_recovered":
    // Do something with the subscription here, like syncing to your database
    console.log(subscription);
    break;
    default:
    throw new Error(`🤷‍♀️ Unhandled event: ${eventName}`);
    }
    } catch (error: unknown) {
    if (typeof error === "string") {
    return new Response("Webhook error", {
    status: 400,
    });
    }

    if (error instanceof Error) {
    return new Response(`Webhook error: ${error.message}`, {
    status: 400,
    });
    }

    throw error;
    }

    return new Response(null, {
    status: 200,
    });
    };