Skip to content

Instantly share code, notes, and snippets.

@amosbastian
Last active August 6, 2024 20:08
Show Gist options
  • Save amosbastian/e403e1d8ccf4f7153f7840dd11a85a69 to your computer and use it in GitHub Desktop.
Save amosbastian/e403e1d8ccf4f7153f7840dd11a85a69 to your computer and use it in GitHub Desktop.
Lemon Squeezy webhook using the new route handler in Next.js 13
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];
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 =
| "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;
};
// Possibly not accurate: it's missing the relationships field and any custom data you add
data: LemonsqueezySubscription;
};
export const POST = async (request: NextRequest) => {
try {
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(request.headers.get("x-signature") as string, "utf8");
if (!crypto.timingSafeEqual(digest, signature)) {
return new Response("Invalid signature.", {
status: 400,
});
}
const payload = JSON.parse(text);
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 (isError(error)) {
return new Response(`Webhook error: ${error.message}`, {
status: 400,
});
}
return new Response("Webhook error", {
status: 400,
});
}
return new Response(null, {
status: 200,
});
};
@soulbliss
Copy link

Thanks a lot, long live open source!

@sulaimanwebdev
Copy link

Best :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment